feat(se): Move 2 W1 — SE generation detection, gen-isolated backend, gen-1 page-8 gate
What changed, and why it matters
This is a large firmware commit for the Keystone 3 hardware wallet that restructures how the secure element (SE) chip is used. It introduces a generation-aware backend so the firmware can support two different SE configurations (gen-1, the current fielded design, and gen-2, a new production design) in one binary. The patch moves legacy gen-1 key derivation into its own file, adds a gen-2 backend with a different key-derivation scheme, adds lifecycle status pages so interrupted wallet create/change-PIN/delete operations can be safely resumed or erased at boot, and adds UI flows for weak-passcode warnings and a 'forget password' ownership-proof step. It also gates the legacy page-8 wipe to gen-1 only so it cannot accidentally erase gen-2 data. The commit is described by its authors as scaffolding with no runtime change for current gen-1 devices, but it does add substantial new crypto and state-machine code.
Treat this as a high-touch crypto refactor rather than a routine feature. Reviewers should verify: (1) the gen-2 manifest constants in ResolveSeGen exactly match Atecc608bWriteConfigGen2 and the factory gate, (2) no code path calls Atecc608bKdfNoAuth/DeriveKeyNoAuth on gen-1 slots, (3) SeBackend() NULL returns are handled fail-closed everywhere, (4) the K608 global is cleared on every error path between derive_608 and on_unlock_success, (5) SE_ArmProvisionRecovery does not retain a password past lock/logout/wipe boundaries, (6) the BUILD_PRODUCTION boot-update bypass is not present in release builds, and (7) the weak-passcode duplicate-check counter cannot be reset by an attacker to bypass the 10-attempt limit. Because the commit bundles many changes, it should be split or at least reviewed commit-by-commit in the PR history.
Security signals we found
Generation-aware SE backend with fail-closed NULL backend for UNPROVISIONED/INVALID chips
Legacy page-8 PIN-hash wipe gated to gen-1 only to avoid erasing gen-2 R_wrapped
Per-account lifecycle status pages for atomic crash recovery of create/change-PIN/delete
Gen-2 no-auth KDF/DeriveKey helpers added for slots configured without ReqAuth
Match-count attempt-limit and re-arm logic tied to SE monotonic counter
Weak passcode/password modal and 10-attempt duplicate-check flow added
Forget-password prove-ownership requires a different wallet's password on gen-2
Provision recovery arm/disarm lifecycle to avoid persisting another wallet's password
ClearSecretCache now also clears SE-side transient session secrets (gen-2 K608)
Boot brick check asserts counter < match_count for gen-2
Debug-only boot-update bypass added under BUILD_PRODUCTION guard
Evidence from the diff
The commit adds SeGen_t detection from the locked ATECC608B config manifest, a SeAccountBackend vtable dispatcher (SeBackend()), and isolated gen-1/gen-2 backend implementations. Gen-1 keeps the existing per-account authorize/roll/host KDF derivation and moves the legacy page-8 PIN-hash wipe into a boot_migrate hook. Gen-2 introduces a no-auth KDF/DeriveKey path, a shared device-wide reset key R stored encrypted in slot 13, per-account R_wrapped on DS28S60 page 8, a match_count attempt-limit mechanism using the SE counter, and lifecycle status pages (CREATING/CREATED/CHANGING_PIN/DELETING) outside each account’s data block for atomic crash recovery. New UI/UX code adds weak passcode/password modal, a 10-attempt duplicate-check limit that drops the set-passcode flow, and forget-password prove-ownership that requires authentication via a different wallet on gen-2. Several bug fixes are bundled: correct error propagation in SaveAccountSecret’s master-fingerprint path, clearing leaked simpleResponse, disarming provision recovery on lock/logout, and preventing mid-operation auto-lock teardown.
Changed components
src/managers/se_manager.c/hsrc/managers/se_backend_gen1.csrc/managers/se_backend_gen2.csrc/managers/se_account_backend.hsrc/managers/account_manager.csrc/managers/keystore.csrc/managers/screen_manager.csrc/driver/drv_atecc608b.c/hsrc/crypto/secret_cache.csrc/device_settings.csrc/main.csrc/ui/gui_model/gui_model.csrc/ui/gui_widgets/gui_enter_passcode.csrc/ui/gui_components/gui_keyboard_hintbox.c/hsrc/ui/gui_widgets/gui_create_wallet_widgets.csrc/ui/gui_views/gui_forget_pass_view.csrc/ui/gui_views/gui_views.hsrc/ui/gui_widgets/gui_about_info_widgets.csrc/config/version.cInspect captured patch +2163 / −167
diff --git a/src/config/version.c b/src/config/version.c
index acbccd9..71def31 100644
--- a/src/config/version.c
+++ b/src/config/version.c
@@ -87,6 +87,9 @@ void GetBootVersionNumber(char *version)
#ifndef COMPILE_SIMULATOR
bool NeedUpdateBoot(void)
{
+ #ifndef BUILD_PRODUCTION
+ return false;
+ #endif
uint32_t major, minor, build;
if (GetBootSoftwareVersion(&major, &minor, &build) == false) {
return true;
diff --git a/src/crypto/secret_cache.c b/src/crypto/secret_cache.c
index 083638a..96d5cf4 100644
--- a/src/crypto/secret_cache.c
+++ b/src/crypto/secret_cache.c
@@ -7,6 +7,7 @@
#include "log_print.h"
#include "stdio.h"
#include "account_manager.h"
+#include "se_manager.h"
static char *g_passwordCache = NULL;
static char *g_newPasswordCache = NULL;
@@ -305,4 +306,7 @@ void ClearSecretCache(void)
memset_s(g_checksumCache, 32, 0, 32);
memset_s(g_diceRollHashCache, 32, 0, 32);
g_diceRollsLen = 0;
+
+ // SE-side transient session secrets share the passcode cache's lifetime (gen-2). No-op on gen-1.
+ SE_ClearSessionSecrets();
}
diff --git a/src/device_settings.c b/src/device_settings.c
index 44d0993..dd39f14 100644
--- a/src/device_settings.c
+++ b/src/device_settings.c
@@ -16,6 +16,7 @@
#include "screen_manager.h"
#include "power_manager.h"
#include "account_manager.h"
+#include "se_manager.h"
#include "version.h"
#include "legacy_web_update_pad.h"
#include "lv_i18n_api.h"
@@ -482,9 +483,17 @@ void WipeDevice(void)
SetShowPowerOffPage(false);
FpWipeManageInfo();
ErasePublicInfo();
+ // gen-2: also wipe the SE-side secrets (gen-1 no-op). Best-effort — the blobs are already wiped + flash
+ // erased below.
+ SE_WipeAll();
DestroyAccount(0);
DestroyAccount(1);
DestroyAccount(2);
+ // Clear the external per-account status pages (36/37/38). DestroyAccount above already clears each, but do
+ // it explicitly so the in-app wipe sweeps them like the bootloader's full-range wipe does.
+ SE_SetAccountStatus(0, ACCOUNT_STATUS_UNKNOWN);
+ SE_SetAccountStatus(1, ACCOUNT_STATUS_UNKNOWN);
+ SE_SetAccountStatus(2, ACCOUNT_STATUS_UNKNOWN);
for (uint32_t addr = 0; addr < GD25QXX_FLASH_SIZE; addr += 1024 * 64) {
Gd25FlashBlockErase(addr);
printf("flash erase address: %#x\n", addr);
diff --git a/src/driver/drv_atecc608b.c b/src/driver/drv_atecc608b.c
index 0d22d58..537dad7 100644
--- a/src/driver/drv_atecc608b.c
+++ b/src/driver/drv_atecc608b.c
@@ -225,6 +225,67 @@ int32_t Atecc608bDeriveKey(uint8_t slot, const uint8_t *authKey)
return ret;
}
+/// @brief No-auth KDF — no Authorize/CheckMac. For gen-2 slots configured without ReqAuth, where the
+/// standard Atecc608bKdf would fail at GetAuthSlot()==255. Same as Atecc608bKdf without the
+/// GetAuthSlot/Atecc608bAuthorize gate.
+/// @param[in] slot source key slot.
+/// @param[in] inData KDF message (transient), inLen bytes.
+/// @param[out] outData 32-byte derived output (decrypted).
+/// @return err code.
+int32_t Atecc608bKdfNoAuth(uint8_t slot, const uint8_t *inData, uint32_t inLen, uint8_t *outData)
+{
+ int32_t ret = ERR_ATECC608B_SLOT_NUM_ERR;
+ uint8_t nonce[32];
+ uint8_t ioProtectKey[32];
+ uint32_t retry;
+
+ do {
+ for (retry = 0; retry < 3; retry++) {
+ ret = atcab_kdf(KDF_MODE_SOURCE_SLOT | KDF_MODE_TARGET_OUTPUT_ENC | KDF_MODE_ALG_HKDF,
+ slot, KDF_DETAILS_HKDF_MSG_LOC_INPUT | (inLen << 24), inData, outData, nonce);
+ if (ret != ATCA_SUCCESS) {
+ continue;
+ }
+ GetIoProtectKey(ioProtectKey);
+ atca_io_decrypt_in_out_t io_dec_params = {
+ .io_key = ioProtectKey,
+ .out_nonce = nonce,
+ .data = outData,
+ .data_size = 32,
+ };
+ ret = atcah_io_decrypt(&io_dec_params);
+ if (ret == ATCA_SUCCESS) {
+ break;
+ }
+ }
+ CHECK_ATECC608B_RET("kdf gen2", ret);
+ } while (0);
+ CLEAR_ARRAY(nonce);
+ CLEAR_ARRAY(ioProtectKey);
+
+ return ret;
+}
+
+/// @brief No-auth DeriveKey — no Authorize. For gen-2 slots configured without ReqAuth.
+/// @param[in] slot Specified slot.
+/// @return err code.
+int32_t Atecc608bDeriveKeyNoAuth(uint8_t slot)
+{
+ int32_t ret;
+ uint8_t nonce[NONCE_NUMIN_SIZE];
+
+ do {
+ TrngGet(nonce, NONCE_NUMIN_SIZE);
+ ret = atcab_nonce_rand(nonce, NULL);
+ CHECK_ATECC608B_RET("nonce rand", ret);
+ ret = atcab_derivekey(0, slot, NULL);
+ CHECK_ATECC608B_RET("derivekey gen2", ret);
+ } while (0);
+ CLEAR_ARRAY(nonce);
+
+ return ret;
+}
+
/// @brief Try to bind SE chip, return a err code if SE chip already binded.
/// @return err code.
static int32_t Atecc608bBinding(void)
@@ -262,7 +323,7 @@ static int32_t Atecc608bBinding(void)
TrngGet(keys, sizeof(keys));
WriteOtpData(OTP_ADDR_ATECC608B, keys, sizeof(keys));
// return 0 for writing config successfully
- ret = Atecc608bWriteConfig();
+ ret = Atecc608bWriteConfigGen2();
} else {
printf("err,OTP key doesn't exist,SE lock\r\n");
ret = ERR_ATECC608B_BIND;
@@ -364,7 +425,8 @@ static void Atecc608bPrintConfig(const Atecc608bConfig_t *config)
PrintU16Array("keyConfig", config->keyConfig, sizeof(config->keyConfig) / 2);
}
-static int32_t Atecc608bWriteConfig(void)
+static int32_t Atecc608bWriteConfig
+(void)
{
//shared keys
//slot 0 ioprotect key, slot config=0x8080, key config=0x007C, lockable=1.
@@ -445,6 +507,85 @@ static int32_t Atecc608bWriteConfig(void)
return ret;
}
+// Canonical gen-2 config manifest -- single source of truth shared by Atecc608bWriteConfigGen2
+// (writer) and Atecc608bVerifyConfigGen2 (factory gate) so the two can never drift. Index = slot 0..15.
+static const uint16_t g_gen2SlotConfig[16] = {
+ 0x8080, 0x8080, 0x8080, 0x42A0, 0x20A0, 0x42A0, 0x42A0, 0x20A0,
+ 0x4D00, 0x42A0, 0x42A0, 0x20A0, 0x42A0, 0x42A0, 0x0083, 0x42C2,
+};
+static const uint16_t g_gen2KeyConfig[16] = {
+ 0x007C, 0x007C, 0x01FC, 0x005C, 0x005C, 0x005C, 0x005C, 0x005C,
+ 0x001C, 0x005C, 0x005C, 0x005C, 0x005C, 0x005C, 0x0013, 0x005C,
+};
+
+/// @brief Write the gen-2 config manifest + blank-chip bootstrap on a BLANK 608B. Mirrors the
+/// firmware's Atecc608bWriteConfigGen2 — the manifest MUST match what firmware GetSeGen()
+/// checks for SE_GEN_2.
+/// Sequence: config + lock_config -> write match_count to slot 8 (MUST be before lock_data
+/// and before any limited-use op, else the affected keys become unusable) -> shared keys
+/// 0/1/2 + genkey 14 -> lock_data.
+/// @return err code.
+int32_t Atecc608bWriteConfigGen2(void)
+{
+ int32_t ret;
+ Atecc608bConfig_t config;
+ bool isLock;
+ uint8_t tempKey[32];
+ uint8_t matchCount[32];
+
+ do {
+ ret = atcab_is_config_locked(&isLock);
+ CHECK_ATECC608B_RET("get lock", ret);
+ if (isLock == true) {
+ printf("already locked\r\n");
+ ret = ERR_ATECC608B_UNEXPECT_LOCK;
+ break;
+ }
+ ret = atcab_read_config_zone((uint8_t *)&config);
+ CHECK_ATECC608B_RET("read config zone", ret);
+ memcpy(config.slotConfig, g_gen2SlotConfig, sizeof(g_gen2SlotConfig));
+ memcpy(config.keyConfig, g_gen2KeyConfig, sizeof(g_gen2KeyConfig));
+ config.countMatch = 0x81; // enable CountMatch, CounterMatchKey = slot 8
+ config.chipOptions = 0x0402; // IO-protection enable — required for TARGET_OUTPUT_ENC
+ ret = atcab_write_config_zone((uint8_t *)&config);
+ CHECK_ATECC608B_RET("write config zone", ret);
+ ret = atcab_lock_config_zone();
+ CHECK_ATECC608B_RET("lock config zone", ret); // CountMatch is now LIVE
+ printf("lock config zone ok\r\n");
+
+ // Write match_count to slot 8 BEFORE lock_data and any limited-use op. The value is written
+ // into two positions of the block (device count-match format). Skipping this leaves the
+ // limited-use keys unusable.
+ memset(matchCount, 0, sizeof(matchCount));
+ matchCount[0] = (uint8_t)SE_GEN2_MATCH_COUNT_INIT;
+ matchCount[4] = (uint8_t)SE_GEN2_MATCH_COUNT_INIT; // count-match format duplicate
+ ret = atcab_write_zone(ATCA_ZONE_DATA, SLOT_MATCH_COUNT, 0, 0, matchCount, 32);
+ CHECK_ATECC608B_RET("bootstrap match_count", ret);
+ printf("bootstrap match_count=%d ok\r\n", SE_GEN2_MATCH_COUNT_INIT);
+
+ GetIoProtectKey(tempKey);
+ ret = atcab_write_zone(ATCA_ZONE_DATA, SLOT_IO_PROTECT_KEY, 0, 0, tempKey, 32);
+ CHECK_ATECC608B_RET("write io protect key", ret);
+ GetAuthKey(tempKey);
+ ret = atcab_write_zone(ATCA_ZONE_DATA, SLOT_AUTH_KEY, 0, 0, tempKey, 32);
+ CHECK_ATECC608B_RET("write auth key", ret);
+ GetEncryptKey(tempKey);
+ ret = atcab_write_zone(ATCA_ZONE_DATA, SLOT_ENCRYPT_KEY, 0, 0, tempKey, 32);
+ CHECK_ATECC608B_RET("write encrypt key", ret);
+ printf("write key ok\r\n");
+ ret = atcab_genkey(SLOT_DEVICE_KEY, NULL);
+ CHECK_ATECC608B_RET("generate unique key", ret);
+ printf("generate device unique key ok\r\n");
+ ret = atcab_lock_data_zone();
+ CHECK_ATECC608B_RET("lock data zone", ret);
+ printf("gen-2 config + bootstrap done\r\n");
+ } while (0);
+
+ CLEAR_ARRAY(tempKey);
+ CLEAR_ARRAY(matchCount);
+ return ret;
+}
+
/// @brief Before using a slot as a key, authorizion is required.
/// @param
/// @return err code.
diff --git a/src/driver/drv_atecc608b.h b/src/driver/drv_atecc608b.h
index 27fb5ed..6a77c17 100644
--- a/src/driver/drv_atecc608b.h
+++ b/src/driver/drv_atecc608b.h
@@ -25,6 +25,11 @@
#define SLOT_DEVICE_KEY 14
#define SLOT_DEVICE_TAMPER_FLAG 15
+// gen-2 factory: the two freed slots take jobs (must match firmware GetSeGen() manifest).
+#define SLOT_MATCH_COUNT 8 // count-match slot (WriteKey = slot 13)
+#define SLOT_RESET_KEY 13 // shared reset key slot
+#define SE_GEN2_MATCH_COUNT_INIT 192 // initial match-count, written in the clear before lock_data
+
#pragma pack(1)
typedef struct __Atecc608bConfig_t {
uint8_t sn1[4];
@@ -63,6 +68,13 @@ int32_t Atecc608bEncryptWrite(uint8_t slot, uint8_t block, const uint8_t *data);
int32_t Atecc608bEncryptRead(uint8_t slot, uint8_t block, uint8_t *data);
int32_t Atecc608bKdf(uint8_t slot, const uint8_t *authKey, const uint8_t *inData, uint32_t inLen, uint8_t *outData);
int32_t Atecc608bDeriveKey(uint8_t slot, const uint8_t *authKey);
+// No Authorize/CheckMac — for gen-2 slots without ReqAuth.
+// GEN2 required
+int32_t Atecc608bKdfNoAuth(uint8_t slot, const uint8_t *inData, uint32_t inLen, uint8_t *outData);
+int32_t Atecc608bDeriveKeyNoAuth(uint8_t slot);
+// gen-2 config manifest writer + blank-chip bootstrap — run on a BLANK chip only.
+int32_t Atecc608bWriteConfigGen2(void);
+// **********
int32_t Atecc608bGenDevicePubkey(uint8_t* pubkey);
int32_t Atecc608bSignMessageWithDeviceKey(uint8_t *messageHash, uint8_t *signature);
void Atecc608bTest(int argc, char *argv[]);
diff --git a/src/main.c b/src/main.c
index f0e540b..61459e1 100644
--- a/src/main.c
+++ b/src/main.c
@@ -38,6 +38,7 @@
#include "user_sqlite3.h"
#include "screen_manager.h"
#include "keystore.h"
+#include "se_manager.h"
#include "log.h"
#include "fingerprint_process.h"
#include "fingerprint_task.h"
@@ -77,6 +78,7 @@ int main(void)
UserMsgInit();
DS28S60_Init();
Atecc608bInit();
+ SeManagerInit(); // resolve SE generation once (after the 608B is up, before any SE-account use)
AccountsDataCheck();
MountUsbFatfs();
RtcInit();
diff --git a/src/managers/account_manager.c b/src/managers/account_manager.c
index d166373..9aa1a5d 100644
--- a/src/managers/account_manager.c
+++ b/src/managers/account_manager.c
@@ -39,26 +39,8 @@ static ZcashUFVKCache_t g_zcashUFVKcache = {0};
static void ClearZcashUFVK();
#endif
-static int32_t WipeLegacyPasswordHashPages(void)
-{
- uint8_t data[32] = {0};
- int32_t ret = SUCCESS_CODE;
-
- if (IsPinHashWiped()) {
- return SUCCESS_CODE;
- }
-
- // Erase old PIN verifiers. Do not add normal read/write users for this page.
- for (uint8_t accountIndex = 0; accountIndex < 3; accountIndex++) {
- ret = SE_HmacEncryptWrite(data, accountIndex * PAGE_NUM_PER_ACCOUNT + PAGE_INDEX_LEGACY_PASSWORD_HASH);
- CHECK_ERRCODE_BREAK("wipe legacy password hash", ret);
- }
- if (ret == SUCCESS_CODE) {
- ret = SetPinHashWiped(true);
- }
- CLEAR_ARRAY(data);
- return ret;
-}
+// The legacy page-8 PIN-hash wipe (gen-1 only) lives in se_backend_gen1.c's boot_migrate, dispatched
+// below via SE_BootMigrate(). The gen-2 backend has no such wipe.
/// @brief Get current account info from SE, and copy info to g_currentAccountInfo.
/// @return err code.
@@ -86,7 +68,9 @@ int32_t AccountManagerInit(void)
ASSERT(sizeof(PublicInfo_t) == 32);
ret = SE_HmacEncryptRead((uint8_t *)&g_publicInfo, PAGE_PUBLIC_INFO);
CHECK_ERRCODE_RETURN_INT(ret);
- ret = WipeLegacyPasswordHashPages();
+ // One-time boot migration, dispatched to the generation backend: gen-1 wipes the legacy page-8 PIN
+ // hash; gen-2 and UNPROVISIONED/INVALID are no-ops. The wipe lives only in se_backend_gen1.c.
+ ret = SE_BootMigrate();
return ret;
}
@@ -239,6 +223,46 @@ int32_t VerifyCurrentAccountPassword(const char *password)
return ret;
}
+uint8_t RecordCurrentPasswordError(uint8_t maxCount)
+{
+ if (g_publicInfo.currentPasswordErrorCount < maxCount) {
+ g_publicInfo.currentPasswordErrorCount++;
+ } else {
+ g_publicInfo.currentPasswordErrorCount = maxCount;
+ }
+ printf("current password error count=%d\r\n", g_publicInfo.currentPasswordErrorCount);
+ SE_HmacEncryptWrite((uint8_t *)&g_publicInfo, PAGE_PUBLIC_INFO);
+ return g_publicInfo.currentPasswordErrorCount;
+}
+
+/// @brief Verify ownership against any wallet and tick the LOGIN error counter WITHOUT logging in.
+/// Used by forget-pass "Prove Device Ownership" so wrong attempts count toward the device wipe
+/// (loginPasswordErrorCount). The count is clamped at maxCount (= MAX_LOGIN_PASSWORD_ERROR_COUNT)
+/// because the modal is escapable and the count persists.
+/// @param[out] accountIndex matched account index on success (may be NULL).
+/// @param[in] password Password string.
+/// @param[in] maxCount clamp ceiling for the login error counter (= MAX_LOGIN_PASSWORD_ERROR_COUNT).
+/// @return err code.
+int32_t VerifyOwnershipPasswordTryAll(uint8_t *accountIndex, const char *password, uint8_t maxCount)
+{
+ int32_t ret = FindAccountByPassword(accountIndex, password);
+ if (ret == SUCCESS_CODE) {
+ g_publicInfo.loginPasswordErrorCount = 0;
+ } else if (ret == ERR_KEYSTORE_PASSWORD_ERR) {
+ if (g_publicInfo.loginPasswordErrorCount < maxCount) {
+ g_publicInfo.loginPasswordErrorCount++;
+ } else {
+ // pin at the wipe threshold (heals a stale over-cap value too).
+ g_publicInfo.loginPasswordErrorCount = maxCount;
+ }
+ printf("prove-ownership login error count=%d\r\n", g_publicInfo.loginPasswordErrorCount);
+ } else {
+ return ret; // transient SE error: don't touch the counter
+ }
+ SE_HmacEncryptWrite((uint8_t *)&g_publicInfo, PAGE_PUBLIC_INFO);
+ return ret;
+}
+
int32_t ClearCurrentPasswordErrorCount(void)
{
printf("clear current password error count\r\n");
@@ -582,11 +606,40 @@ int32_t DestroyAccount(uint8_t accountIndex)
uint8_t data[32] = {0};
ASSERT(accountIndex <= 2);
+ // Mark the account DELETING (external status page, survives the zero-loop below). If power dies mid-erase,
+ // the boot check sees DELETING and finishes the deletion. Cleared to UNKNOWN once the data is gone.
+ printf("destroy account %d\n", accountIndex);
+ ret = SE_SetAccountStatus(accountIndex, ACCOUNT_STATUS_DELETING);
+ if (ret != SUCCESS_CODE) {
+ printf("destroy account:set deleting status err,0x%X\n", ret);
+ CLEAR_ARRAY(data);
+ return ret;
+ }
+ printf("destroy account:set account flag in se %d\n", accountIndex);
for (uint8_t i = 0; i < PAGE_NUM_PER_ACCOUNT; i++) {
printf("erase index=%d\n", i);
ret = SE_HmacEncryptWrite(data, accountIndex * PAGE_NUM_PER_ACCOUNT + i);
CHECK_ERRCODE_BREAK("ds28s60 write", ret);
}
+ // Only declare the deletion complete if the page-erase loop actually finished. If a write failed partway,
+ // leave the status at DELETING (do NOT roll to UNKNOWN) so the next boot's AccountsDataCheck finishes the
+ // deletion — exactly as it would after a power-loss. Clearing the marker on a write error would lose that
+ // resume guarantee and could resurrect a half-erased account via the coarse 2-sentinel boot check.
+ if (ret == SUCCESS_CODE) {
+ // gen-2: erase this account's SE-side key material, not just the pages zeroed above (gen-1/simulator
+ // no-op). Only clear DELETING after this succeeds; otherwise boot retries cleanup.
+ ret = SE_EraseAccount(accountIndex);
+ if (ret != SUCCESS_CODE) {
+ printf("destroy account:erase se account err,0x%X\n", ret);
+ } else {
+ ret = SE_SetAccountStatus(accountIndex, ACCOUNT_STATUS_UNKNOWN); // deletion complete -> blank
+ if (ret != SUCCESS_CODE) {
+ printf("destroy account:clear deleting status err,0x%X\n", ret);
+ } else {
+ printf("destroy account:clear se done %d\n", accountIndex);
+ }
+ }
+ }
DeleteAccountPublicInfo(accountIndex);
ClearAccountPassphrase(accountIndex);
SetWalletDataHash(accountIndex, data);
@@ -594,7 +647,11 @@ int32_t DestroyAccount(uint8_t accountIndex)
CLEAR_OBJECT(g_currentAccountInfo);
CLEAR_ARRAY(data);
-
+ if (ret == SUCCESS_CODE) {
+ printf("destroy account %d all set\n", accountIndex);
+ } else {
+ printf("destroy account %d finished with err,0x%X\n", accountIndex, ret);
+ }
return ret;
}
// wipe device may power lose, check the account status.
@@ -605,6 +662,44 @@ void AccountsDataCheck(void)
uint8_t data[32], accountIndex, validCount, i;
for (accountIndex = 0; accountIndex < 3; accountIndex++) {
+ // gen-2 lifecycle status (external page): an account caught mid-create / mid-change-PIN / mid-delete is
+ // inconsistent -> erase it. delete -> finishes the deletion; change-PIN -> user restores from seed;
+ // create -> drops the partial wallet. A valid CREATED account skips the coarse heuristic below.
+ AccountStatus_t status = ACCOUNT_STATUS_UNKNOWN;
+ if (SE_GetAccountStatus(accountIndex, &status) == SUCCESS_CODE) {
+ if (status == ACCOUNT_STATUS_CREATING || status == ACCOUNT_STATUS_CHANGING_PIN ||
+ status == ACCOUNT_STATUS_DELETING) {
+ printf("incomplete op on account %d (status=%d) -> erase\n", accountIndex, status);
+ memset_s(data, sizeof(data), 0, sizeof(data));
+ ret = SUCCESS_CODE;
+ for (i = 0; i < PAGE_NUM_PER_ACCOUNT; i++) {
+ ret = SE_HmacEncryptWrite(data, accountIndex * PAGE_NUM_PER_ACCOUNT + i);
+ if (ret != SUCCESS_CODE) {
+ printf("incomplete op erase page err,account=%d,page=%d,err=0x%X\n", accountIndex, i, ret);
+ break;
+ }
+ }
+ if (ret == SUCCESS_CODE) {
+ // gen-2: erase this account's SE-side key material, mirroring DestroyAccount's cleanup (the
+ // page-zeroing above only clears the blob). Gen-1/simulator no-op; clear status only after
+ // it succeeds.
+ ret = SE_EraseAccount(accountIndex);
+ if (ret != SUCCESS_CODE) {
+ printf("incomplete op erase se account err,account=%d,err=0x%X\n", accountIndex, ret);
+ } else {
+ ret = SE_SetAccountStatus(accountIndex, ACCOUNT_STATUS_UNKNOWN); // account now blank
+ if (ret != SUCCESS_CODE) {
+ printf("incomplete op clear status err,account=%d,err=0x%X\n", accountIndex, ret);
+ }
+ }
+ }
+ continue;
+ }
+ if (status == ACCOUNT_STATUS_CREATED) {
+ continue; // explicitly valid
+ }
+ }
+ // status UNKNOWN (legacy / pre-status accounts) -> original coarse 2-sentinel check.
validCount = 0;
// for se gen1, check each account start
ret = SE_HmacEncryptRead(data, accountIndex * PAGE_NUM_PER_ACCOUNT + PAGE_INDEX_IV);
diff --git a/src/managers/account_manager.h b/src/managers/account_manager.h
index 97bb87b..1404071 100644
--- a/src/managers/account_manager.h
+++ b/src/managers/account_manager.h
@@ -64,6 +64,8 @@ int32_t CreateNewAccount(uint8_t accountIndex, const uint8_t *entropy, uint8_t e
int32_t CreateNewSlip39Account(uint8_t accountIndex, const uint8_t *ems, const uint8_t *entropy, uint8_t entropyLen, const char *password, uint16_t id, bool eb, uint8_t ie);
int32_t ClearCurrentPasswordErrorCount(void);
int32_t VerifyCurrentAccountPassword(const char *password);
+uint8_t RecordCurrentPasswordError(uint8_t maxCount);
+int32_t VerifyOwnershipPasswordTryAll(uint8_t *accountIndex, const char *password, uint8_t maxCount);
int32_t VerifyPasswordAndLogin(uint8_t *accountIndex, const char *password);
void LogoutCurrentAccount(void);
uint8_t GetCurrentAccountIndex(void);
diff --git a/src/managers/keystore.c b/src/managers/keystore.c
index 94cf7be..70aa04c 100644
--- a/src/managers/keystore.c
+++ b/src/managers/keystore.c
@@ -280,11 +280,23 @@ int32_t ChangePassword(uint8_t accountIndex, const char *newPassword, const char
do {
ret = CheckPasswordExisted(newPassword, accountIndex);
CHECK_ERRCODE_BREAK("check repeat password", ret);
- ret = LoadAccountSecret(accountIndex, &accountSecret, password);
+ ret = LoadAccountSecret(accountIndex, &accountSecret, password); // decrypt seed under PIN_old (reads only)
CHECK_ERRCODE_BREAK("load account secret", ret);
+ // Change-PIN re-provisions the SAME index under PIN_new. Bracket the re-wrap with CHANGING_PIN so an
+ // interrupted change (power loss / failure) is erased and restored at boot. The backend hook
+ // handles generation-specific prep (gen-1 no-op).
+ ret = SE_SetAccountStatus(accountIndex, ACCOUNT_STATUS_CHANGING_PIN);
+ CHECK_ERRCODE_BREAK("set changing pin status", ret);
+ ret = SE_PrepareChangePin(accountIndex, password); // password = PIN_old
+ CHECK_ERRCODE_BREAK("prepare change pin", ret);
ret = SaveAccountSecret(accountIndex, &accountSecret, newPassword, false);
- CHECK_ERRCODE_BREAK("save account secret", ret);
+ CHECK_ERRCODE_BREAK("save account secret", ret); // on failure: status stays CHANGING_PIN -> boot erases
+ ret = SE_SetAccountStatus(accountIndex, ACCOUNT_STATUS_CREATED); // re-wrap committed
+ CHECK_ERRCODE_BREAK("set created status", ret);
} while (0);
+ // SetNewKeyPieceToSE consumes the arm on the normal path; disarm here too so a change-PIN never
+ // leaves it armed (e.g. if SaveAccountSecret returned before SetNewKeyPieceToSE).
+ SE_DisarmProvisionRecovery();
CLEAR_OBJECT(accountSecret);
return ret;
}
@@ -523,6 +535,15 @@ static int32_t SaveAccountSecret(uint8_t accountIndex, const AccountSecret_t *ac
#ifdef COMPILE_SIMULATOR
ret = SimulatorSaveAccountSecret(accountIndex, accountSecret, password);
#else
+ if (newAccount) {
+ // Account-create bracket: mark CREATING before any SE write, so an
+ // interrupted create (power loss / auto-lock mid-provision) is caught at the next boot
+ // (AccountsDataCheck erases the partial account) instead of leaving a half-written, HMAC-failing
+ // slot. Flipped to CREATED only after the full blob + account info commit below. (Change-PIN's
+ // CHANGING_PIN bracket lives in ChangePassword; it calls here with newAccount == false.)
+ ret = SE_SetAccountStatus(accountIndex, ACCOUNT_STATUS_CREATING);
+ CHECK_ERRCODE_BREAK("set creating status", ret);
+ }
ret = SetNewKeyPieceToSE(accountIndex, pieces, password);
CHECK_ERRCODE_BREAK("set key to se", ret);
HashWithSalt(hash, pieces, sizeof(pieces), "combine two pieces");
@@ -571,6 +592,7 @@ static int32_t SaveAccountSecret(uint8_t accountIndex, const AccountSecret_t *ac
SimpleResponse_u8 *simpleResponse = get_master_fingerprint((PtrBytes)accountSecret->seed, seedLen);
if (simpleResponse == NULL) {
printf("get_master_fingerprint return NULL\r\n");
+ ret = ERR_GENERAL_FAIL; // a bare break only exits the switch -> set ret, propagated below
break;
}
if (simpleResponse->error_code != 0) {
@@ -578,8 +600,10 @@ static int32_t SaveAccountSecret(uint8_t accountIndex, const AccountSecret_t *ac
if (simpleResponse->error_message != NULL) {
printf("error code = %d\r\nerror msg is: %s\r\n", simpleResponse->error_code, simpleResponse->error_message);
}
+ ret = simpleResponse->error_code;
+ free_simple_response_u8(simpleResponse); // was leaked on the error path
+ break;
}
- CHECK_ERRCODE_BREAK("get_master_fingerprint", simpleResponse->error_code);
uint8_t *masterFingerprint = simpleResponse->data;
PrintArray("masterFingerprint", masterFingerprint, 4);
SetCurrentAccountMfp(masterFingerprint);
@@ -590,8 +614,14 @@ static int32_t SaveAccountSecret(uint8_t accountIndex, const AccountSecret_t *ac
// mfp is 0x00000000;
break;
}
- SaveCurrentAccountInfo();
+ // The breaks inside the switch above terminate the switch, NOT this do/while, so a fingerprint
+ // failure (NULL response or non-zero error_code) must be propagated here before the account is
+ // committed — otherwise execution falls through and marks a bad account CREATED.
+ CHECK_ERRCODE_BREAK("get_master_fingerprint", ret);
+ ret = SaveCurrentAccountInfo(); // capture the param-page write result (was discarded)
CHECK_ERRCODE_BREAK("write param", ret);
+ ret = SE_SetAccountStatus(accountIndex, ACCOUNT_STATUS_CREATED); // create fully committed
+ CHECK_ERRCODE_BREAK("set created status", ret);
}
} while (0);
@@ -657,6 +687,9 @@ static int32_t LoadAccountSecretFromSE(uint8_t accountIndex, AccountSecret_t *ac
KEYSTORE_PRINT_ARRAY("authKey", authKey, AUTH_KEY_LEN);
ret = ((memcmp(hmacCalc, hmac, HMAC_LEN) == 0) ? SUCCESS_CODE : ERR_KEYSTORE_AUTH);
CHECK_ERRCODE_BREAK("check hmac", ret);
+ // PIN proven correct here -> notify the SE backend of a successful unlock (gen-1: no-op).
+ ret = SE_OnUnlockSuccess(accountIndex);
+ CHECK_ERRCODE_BREAK("on unlock success", ret);
accountSecret->entropyLen = (pAccountInfo->entropyLen == 0) ? 32 : pAccountInfo->entropyLen; // 32 bytes as default.
CombineInnerAesKey(enKey);
AES256_CBC_init(&ctx, enKey, iv);
diff --git a/src/managers/screen_manager.c b/src/managers/screen_manager.c
index 9874a30..8f0cab2 100644
--- a/src/managers/screen_manager.c
+++ b/src/managers/screen_manager.c
@@ -14,6 +14,7 @@
#include "power_manager.h"
#include "gui_lock_widgets.h"
#include "account_manager.h"
+#include "se_manager.h"
#include "fingerprint_process.h"
#define LOCK_SCREEN_TICK 1000
@@ -112,6 +113,10 @@ static void LockScreen(void)
return;
}
+ // Session boundary: the device is locking (inactivity timer or power button both reach here).
+ // Disarm any pending provision recovery before locking.
+ SE_DisarmProvisionRecovery();
+
static uint16_t single = SIG_LOCK_VIEW_VERIFY_PIN;
uint8_t accountNum = 1;
diff --git a/src/managers/se_account_backend.h b/src/managers/se_account_backend.h
new file mode 100644
index 0000000..058661b
--- /dev/null
+++ b/src/managers/se_account_backend.h
@@ -0,0 +1,55 @@
+#ifndef _SE_ACCOUNT_BACKEND_H_
+#define _SE_ACCOUNT_BACKEND_H_
+
+#include "stdint.h"
+
+// Generation-isolated SE account backend.
+// One const vtable per SE generation; SeBackend() selects it once from GetSeGen(). Gen-specific code
+// lives behind these pointers; shared crypto (combine/HMAC/AES, DS piece, try-all) stays in the shared
+// layer. All SE-account calls MUST route through SeBackend() (never the gen1/gen2 symbols directly) so the
+// single binary never runs the wrong-generation path.
+typedef struct {
+ // 608-side key piece — the only derivation that differs between generations.
+ // ⚠️ ARG ORDER: (accountIndex, password, piece608) — password BEFORE the output buffer. This is the
+ // REVERSE of the legacy GetKeyPieceFromAtecc608b(accountIndex, piece, password). Passing them swapped
+ // compiles (uint8_t* vs const char* is warn-only) but hashes the wrong buffer as the password and
+ // bricks unlock with 608 CheckMac 0xD1. Always call as derive_608(idx, password, pieces).
+ int32_t (*derive_608)(uint8_t accountIndex, const char *password, uint8_t piece608[32]); // verify/login
+ // create account. R = existing device-wide key to reuse (add-wallet, from an existing wallet),
+ // or NULL = new device -> generate a fresh one. Same ⚠️ ARG ORDER caveat as derive_608 for (password, piece608).
+ int32_t (*provision_608)(uint8_t accountIndex, const char *password, uint8_t piece608[32], const uint8_t *R);
+
+ // Recover the device-wide key R from an ALREADY-PROVISIONED wallet, so a new wallet can be
+ // provisioned with it (add-wallet). existingIdx/existingPassword identify+authenticate that wallet.
+ // gen-2: derives R from that wallet's SE state + password. gen-1: ERR (no R). Caller CLEAR_ARRAY(R_out).
+ int32_t (*recover_reset_key)(uint8_t existingIdx, const char *existingPassword, uint8_t *R_out);
+
+ // Called once after the shared HMAC has verified the unlock.
+ // gen-2: restore per-session SE state. gen-1: no-op.
+ int32_t (*on_unlock_success)(uint8_t accountIndex);
+
+ // Lifecycle. Change-PIN reuses derive_608 + provision_608, but gen-2 must first prepare state from
+ // the old PIN; gen-1 is a no-op. The account-status state machine remains in keystore.c ChangePassword.
+ int32_t (*prepare_change_pin)(uint8_t accountIndex, const char *oldPassword);
+ int32_t (*erase_account)(uint8_t accountIndex); // forget one wallet
+ int32_t (*wipe_all)(void); // full wipe
+
+ // One-time boot work. gen-1: legacy page-8 PIN-hash wipe. gen-2: no-op (page 8 holds other data).
+ int32_t (*boot_migrate)(void);
+
+ // Wipe transient session secrets. Called from ClearSecretCache() (and lock/logout) so SE-side session
+ // material shares the passcode cache's lifetime — never outliving the operation.
+ // gen-2: clear the stashed session key. gen-1: no-op (no session secret).
+ void (*clear_session)(void);
+} SeAccountBackend;
+
+// Each generation's backend is defined in its own isolated file (se_backend_gen1.c / se_backend_gen2.c);
+// the SeBackend() dispatcher lives in se_manager.c and selects between them from GetSeGen().
+extern const SeAccountBackend g_seBackendGen1;
+extern const SeAccountBackend g_seBackendGen2;
+
+// Returns the vtable for the detected generation, or NULL for SE_GEN_UNPROVISIONED / SE_GEN_INVALID
+// (callers must fail closed — a shipped device is always SE_GEN_1 or SE_GEN_2).
+const SeAccountBackend *SeBackend(void);
+
+#endif
diff --git a/src/managers/se_backend_gen1.c b/src/managers/se_backend_gen1.c
new file mode 100644
index 0000000..b7e9d16
--- /dev/null
+++ b/src/managers/se_backend_gen1.c
@@ -0,0 +1,181 @@
+#include <stdio.h>
+#include "string.h"
+#include "se_manager.h"
+#include "se_interface.h"
+#include "se_account_backend.h"
+#include "user_utils.h"
+#include "user_memory.h" // safec strnlen_s macro (-> _strnlen_s_chk)
+#include "assert.h"
+#include "sha256.h"
+#include "err_code.h"
+#include "drv_trng.h"
+#include "drv_atecc608b.h"
+#include "cryptoauthlib.h"
+#include "hash_and_salt.h"
+#include "secret_cache.h"
+#include "account_manager.h" // IsPinHashWiped / SetPinHashWiped (legacy page-8 wipe, boot_migrate)
+
+// gen-1 SE account backend (legacy: per-account Authorize + roll/host KDF).
+// Isolated from se_manager.c, mirroring se_backend_gen2.c. Selected by SeBackend() only when
+// GetSeGen() == SE_GEN_1. The shared DS28S60 key piece + dispatchers stay in se_manager.c.
+
+#define SHA256_COUNT 3
+
+static int32_t NormalizeAteccAuthError(int32_t ret)
+{
+ return (ret == ATCA_CHECKMAC_VERIFY_FAILED) ? ERR_KEYSTORE_AUTH : ret;
+}
+
+// gen-1 608 provisioning: write per-account auth key, derive the roll-kdf slot, write a host-random kdf
+// slot, then KDF(roll)->KDF(host)->3x sha256 to produce the 608 piece.
+static int32_t SetNewKeyPieceToAtecc608b(uint8_t accountIndex, uint8_t *piece, const char *password)
+{
+ uint8_t authKey[32], hostRandom[32], inData[32], outData[32];
+ int32_t ret;
+ AccountSlot_t accountSlot;
+
+ ASSERT(accountIndex <= 2);
+ do {
+ HashWithSalt(authKey, (uint8_t *)password, strnlen_s(password, PASSWORD_MAX_LEN), "auth_key");
+ GetAccountSlot(&accountSlot, accountIndex);
+ ret = SE_EncryptWrite(accountSlot.auth, 0, authKey);
+ CHECK_ERRCODE_BREAK("write auth", ret);
+ ret = SE_DeriveKey(accountSlot.rollKdf, authKey);
+ CHECK_ERRCODE_BREAK("derive key", ret);
+ TrngGet(hostRandom, 32);
+ ret = SE_EncryptWrite(accountSlot.hostKdf, 0, hostRandom);
+ CHECK_ERRCODE_BREAK("write kdf", ret);
+
+ HashWithSalt(outData, (uint8_t *)password, strnlen_s(password, PASSWORD_MAX_LEN), "password_atecc608b");
+ memcpy(inData, outData, 32);
+ ret = SE_Kdf(accountSlot.rollKdf, authKey, inData, 32, outData);
+ CHECK_ERRCODE_BREAK("kdf", ret);
+ memcpy(inData, outData, 32);
+ ret = SE_Kdf(accountSlot.hostKdf, authKey, inData, 32, outData);
+ CHECK_ERRCODE_BREAK("kdf", ret);
+ for (uint32_t i = 0; i < SHA256_COUNT; i++) {
+ memcpy(inData, outData, 32);
+ sha256((struct sha256 *)outData, inData, 32);
+ }
+ memcpy(piece, outData, 32);
+ } while (0);
+ CLEAR_ARRAY(authKey);
+ CLEAR_ARRAY(hostRandom);
+ CLEAR_ARRAY(inData);
+ CLEAR_ARRAY(outData);
+
+ return ret;
+}
+
+// gen-1 608 derive: KDF(roll)->KDF(host)->3x sha256, matching the provisioning above.
+static int32_t GetKeyPieceFromAtecc608b(uint8_t accountIndex, uint8_t *piece, const char *password)
+{
+ uint8_t authKey[32], inData[32], outData[32];
+ int32_t ret;
+ AccountSlot_t accountSlot;
+
+ ASSERT(accountIndex <= 2);
+ do {
+ HashWithSalt(authKey, (uint8_t *)password, strnlen_s(password, PASSWORD_MAX_LEN), "auth_key");
+ HashWithSalt(outData, (uint8_t *)password, strnlen_s(password, PASSWORD_MAX_LEN), "password_atecc608b");
+ memcpy(inData, outData, 32);
+
+ GetAccountSlot(&accountSlot, accountIndex);
+ ret = SE_Kdf(accountSlot.rollKdf, authKey, inData, 32, outData);
+ ret = NormalizeAteccAuthError(ret);
+ CHECK_ERRCODE_BREAK("kdf", ret);
+ memcpy(inData, outData, 32);
+ ret = SE_Kdf(accountSlot.hostKdf, authKey, inData, 32, outData);
+ ret = NormalizeAteccAuthError(ret);
+ CHECK_ERRCODE_BREAK("kdf", ret);
+ for (uint32_t i = 0; i < SHA256_COUNT; i++) {
+ memcpy(inData, outData, 32);
+ sha256((struct sha256 *)outData, inData, 32);
+ }
+ memcpy(piece, outData, 32);
+ } while (0);
+ CLEAR_ARRAY(authKey);
+ CLEAR_ARRAY(inData);
+ CLEAR_ARRAY(outData);
+
+ return ret;
+}
+
+// ============================================================================
+// gen-1 vtable. derive/provision/on_unlock are real; recover_reset_key /
+// erase_account / wipe_all are no-ops on gen-1; boot_migrate does the legacy page-8 wipe.
+// ============================================================================
+static int32_t Gen1Derive608(uint8_t accountIndex, const char *password, uint8_t piece608[32])
+{
+ return GetKeyPieceFromAtecc608b(accountIndex, piece608, password);
+}
+static int32_t Gen1Provision608(uint8_t accountIndex, const char *password, uint8_t piece608[32], const uint8_t *R)
+{
+ (void)R; // unused on gen-1
+ return SetNewKeyPieceToAtecc608b(accountIndex, piece608, password);
+}
+static int32_t Gen1OnUnlockSuccess(uint8_t accountIndex)
+{
+ (void)accountIndex;
+ return SUCCESS_CODE;
+}
+static int32_t Gen1RecoverResetKey(uint8_t existingIdx, const char *existingPassword, uint8_t *R_out)
+{
+ (void)existingIdx; (void)existingPassword; (void)R_out;
+ return SUCCESS_CODE; // no-op on gen-1
+}
+static int32_t Gen1PrepareChangePin(uint8_t accountIndex, const char *oldPassword)
+{
+ (void)accountIndex;
+ (void)oldPassword;
+ return SUCCESS_CODE; // no-op on gen-1
+}
+static int32_t Gen1EraseAccount(uint8_t accountIndex)
+{
+ (void)accountIndex;
+ return SUCCESS_CODE; // gen-1 erase = the DS28S60 page-zeroing in DestroyAccount
+}
+static int32_t Gen1WipeAll(void)
+{
+ return SUCCESS_CODE; // gen-1 wipe = page-zeroing + flash erase in WipeDevice
+}
+// Legacy page-8 PIN-hash wipe — gen-1 only. Idempotent via the pinHashWiped flag.
+static int32_t WipeLegacyPasswordHashPages(void)
+{
+ uint8_t data[32] = {0};
+ int32_t ret = SUCCESS_CODE;
+
+ if (IsPinHashWiped()) {
+ return SUCCESS_CODE;
+ }
+ // Erase old PIN verifiers. Do not add normal read/write users for this page.
+ for (uint8_t accountIndex = 0; accountIndex < 3; accountIndex++) {
+ ret = SE_HmacEncryptWrite(data, accountIndex * PAGE_NUM_PER_ACCOUNT + PAGE_INDEX_LEGACY_PASSWORD_HASH);
+ CHECK_ERRCODE_BREAK("wipe legacy password hash", ret);
+ }
+ if (ret == SUCCESS_CODE) {
+ ret = SetPinHashWiped(true);
+ }
+ CLEAR_ARRAY(data);
+ return ret;
+}
+static int32_t Gen1BootMigrate(void)
+{
+ return WipeLegacyPasswordHashPages(); // one-time legacy page-8 PIN-hash wipe (gen-1 only)
+}
+static void Gen1ClearSession(void)
+{
+ // gen-1 holds no SE-side session secret — nothing to wipe.
+}
+
+const SeAccountBackend g_seBackendGen1 = {
+ .derive_608 = Gen1Derive608,
+ .provision_608 = Gen1Provision608,
+ .recover_reset_key = Gen1RecoverResetKey,
+ .on_unlock_success = Gen1OnUnlockSuccess,
+ .prepare_change_pin = Gen1PrepareChangePin,
+ .erase_account = Gen1EraseAccount,
+ .wipe_all = Gen1WipeAll,
+ .boot_migrate = Gen1BootMigrate,
+ .clear_session = Gen1ClearSession,
+};
diff --git a/src/managers/se_backend_gen2.c b/src/managers/se_backend_gen2.c
new file mode 100644
index 0000000..522f28b
--- /dev/null
+++ b/src/managers/se_backend_gen2.c
@@ -0,0 +1,439 @@
+#include <stdio.h>
+#include "stdlib.h"
+#include "string.h"
+#include "se_manager.h"
+#include "se_interface.h"
+#include "se_account_backend.h"
+#include "account_manager.h"
+#include "user_utils.h"
+#include "user_memory.h"
+#include "assert.h"
+#include "sha256.h"
+#include "hmac.h"
+#include "err_code.h"
+#include "drv_trng.h"
+#include "drv_atecc608b.h"
+#include "cryptoauthlib.h"
+#include "log_print.h"
+#include "hash_and_salt.h"
+#include "secret_cache.h" // PASSWORD_MAX_LEN
+
+// gen-2 SE account backend — isolated from se_manager.c.
+// Selected by SeBackend() (se_manager.c) only when GetSeGen() == SE_GEN_2.
+
+// ============================================================================
+// attempt-Limit (match_count) helpers.
+// ============================================================================
+#define SE_MATCH_COUNT_N 192
+#define SE_MATCH_COUNT_R 1
+
+// Round up to a multiple of 32.
+static uint32_t AlignUp32(uint32_t v)
+{
+ return (v + 31u) & ~31u;
+}
+
+int32_t SE_GetCounter(uint32_t *counter)
+{
+ return atcab_counter_read(0, counter); // Counter0
+}
+
+int32_t SE_GetMatchCount(uint32_t *matchCount)
+{
+ uint8_t s8[32];
+ int32_t ret = atcab_read_zone(ATCA_ZONE_DATA, SLOT_MATCH_COUNT, 0, 0, s8, sizeof(s8));
+ if (ret == ATCA_SUCCESS) {
+ *matchCount = (uint32_t)s8[0] | ((uint32_t)s8[1] << 8) |
+ ((uint32_t)s8[2] << 16) | ((uint32_t)s8[3] << 24);
+ }
+ CLEAR_ARRAY(s8);
+ return ret;
+}
+
+// Encrypted-write the threshold to slot 8 (WriteKey = slot 13). The 32-bit value is stored in bytes 0-3
+// and duplicated in bytes 4-7 as the slot's write format requires.
+int32_t SE_SetMatchCount(uint32_t matchCount, const uint8_t *R)
+{
+ uint8_t data[32];
+ uint8_t numIn[NONCE_NUMIN_SIZE];
+ memset(data, 0, sizeof(data));
+ for (int i = 0; i < 4; i++) {
+ data[i] = (uint8_t)(matchCount >> (8 * i));
+ data[4 + i] = data[i];
+ }
+ TrngGet(numIn, NONCE_NUMIN_SIZE); // host nonce for the encrypted write
+ int32_t ret = atcab_write_enc(SLOT_MATCH_COUNT, 0, data, R, SLOT_RESET_KEY, numIn);
+ CLEAR_ARRAY(data);
+ CLEAR_ARRAY(numIn);
+ return ret;
+}
+
+// Re-arm the attempt Limit after a successful derivation. R must be the recovered plaintext reset key.
+int32_t SE_RearmMatchCount(const uint8_t *R)
+{
+ uint32_t c = 0;
+ int32_t ret = SE_GetCounter(&c);
+ if (ret != ATCA_SUCCESS) {
+ return ret;
+ }
+ uint32_t B = AlignUp32(c + SE_MATCH_COUNT_R);
+#ifndef BUILD_PRODUCTION
+ // [SEARM] DEBUG: dump Counter0 vs match_count right before the match_count write. If Counter0 has caught
+ // up to match_count(cur), the SE refuses the write with ATCA_EXECUTION_ERROR (0xF4).
+ uint32_t curMatch = 0;
+ (void)SE_GetMatchCount(&curMatch); // best-effort read for the log
+ printf("[SEARM] Counter0=%u match_count(cur)=%u match_count(new)=%u\r\n",
+ (unsigned)c, (unsigned)curMatch, (unsigned)(B + SE_MATCH_COUNT_N));
+#endif
+ return SE_SetMatchCount(B + SE_MATCH_COUNT_N, R); // write the new threshold
+}
+
+// ============================================================================
+// derivation helpers.
+// ============================================================================
+// Transient KDF message derived from the PIN. Never stored.
+static void Gen2PinMsg(const char *password, uint8_t out[32])
+{
+ HashWithSalt(out, (uint8_t *)password, strnlen_s(password, PASSWORD_MAX_LEN), "se608bg2-pin-msg");
+}
+
+// Derive K608, the per-account key used to mask R_wrapped on DS28S60.
+static int32_t Gen2DeriveK608(const uint8_t piece608[32], uint8_t k608[32])
+{
+ uint8_t info[8];
+ memcpy(info, "R-wrap", 6);
+ info[6] = 0x01;
+ return (hmac_sha256(piece608, 32, info, 7, k608) == 0) ? SUCCESS_CODE : ERR_GENERAL_FAIL;
+}
+
+// K608 from the most recent successful gen-2 derive, held for on_unlock_success. The all-zero state is the
+// "not set" sentinel (a real key is effectively always non-zero), so there is no separate valid flag. Never
+// touch the buffer directly — go through the Gen2K608* accessors so set / clear / validity stay in one place.
+static uint8_t g_gen2K608[32] = {0};
+
+static void Gen2K608Set(const uint8_t k608[32])
+{
+ memcpy(g_gen2K608, k608, 32);
+}
+
+static void Gen2K608Clear(void)
+{
+ CLEAR_ARRAY(g_gen2K608);
+}
+
+// Copy the stashed K608 into out. Returns ERR_GENERAL_FAIL if it is unset (all-zero). Caller must
+// CLEAR_ARRAY(out) once done.
+static int32_t Gen2K608Get(uint8_t out[32])
+{
+ uint8_t acc = 0;
+ for (int i = 0; i < 32; i++) {
+ acc |= g_gen2K608[i];
+ }
+ if (acc == 0) {
+ return ERR_GENERAL_FAIL;
+ }
+ memcpy(out, g_gen2K608, 32);
+ return SUCCESS_CODE;
+}
+
+// R is never held in a persistent global: recover it into a caller-owned local, use it, CLEAR_ARRAY it on
+// the same call. K608 is the only gen-2 secret that must span two vtable calls (derive_608 ->
+// on_unlock_success), so it alone stays a static (cleared defensively).
+
+// Recover R from this account's page 8 (PAGE_INDEX_R_WRAPPED), unmasking it with the stashed K608. Requires
+// a K608 stashed by a prior Gen2Derive608. Caller must CLEAR_ARRAY(R) once done.
+static int32_t Gen2RecoverR(uint8_t accountIndex, uint8_t R[32])
+{
+ uint8_t wrapped[32], k608[32];
+ int32_t ret;
+
+ ret = Gen2K608Get(k608); // fails if no K608 stashed (wrong order / not derived)
+ if (ret != SUCCESS_CODE) {
+ return ret;
+ }
+ ret = SE_HmacEncryptRead(wrapped, accountIndex * PAGE_NUM_PER_ACCOUNT + PAGE_INDEX_R_WRAPPED);
+ if (ret == SUCCESS_CODE) {
+ // Fail closed if page 8 is blank/zeroed: an all-zero read means this account holds no live R
+ // (destroyed or never provisioned), so recovery fails here instead of returning garbage. Notably
+ // guards the forget-pass case where CreateNewAccount runs DestroyAccount(A) — zeroing A's page 8 —
+ // BEFORE recovery, so recovering from A fails cleanly here instead of pushing garbage on-chip.
+ if (!CheckEntropy(wrapped, 32)) {
+ ret = ERR_GENERAL_FAIL;
+ } else {
+ for (int i = 0; i < 32; i++) {
+ R[i] = wrapped[i] ^ k608[i];
+ }
+ }
+ }
+ CLEAR_ARRAY(wrapped);
+ CLEAR_ARRAY(k608);
+ return ret;
+}
+
+// Wrap R under K608 and store it on the account's page 8. Inverse of Gen2RecoverR. Used by provisioning
+// (and later change-PIN) to (re-)write R_wrapped under a given K608.
+static int32_t Gen2StoreRWrapped(uint8_t accountIndex, const uint8_t R[32], const uint8_t k608[32])
+{
+ uint8_t wrapped[32];
+ int32_t ret;
+ for (int i = 0; i < 32; i++) {
+ wrapped[i] = R[i] ^ k608[i];
+ }
+ ret = SE_HmacEncryptWrite(wrapped, accountIndex * PAGE_NUM_PER_ACCOUNT + PAGE_INDEX_R_WRAPPED);
+ CLEAR_ARRAY(wrapped);
+ return ret;
+}
+
+// ============================================================================
+// gen-2 vtable. Change-PIN has no vtable method — it reuses provision_608 (keystore ChangePassword);
+// boot_migrate is a no-op.
+// ============================================================================
+static int32_t Gen2Derive608(uint8_t accountIndex, const char *password, uint8_t piece608[32])
+{
+ uint8_t inData[32], x[32], k608[32];
+ AccountSlot_t accountSlot;
+ int32_t ret;
+
+ ASSERT(accountIndex <= 2);
+ GetAccountSlot(&accountSlot, accountIndex); // per-account ROLL_KDF slot
+ Gen2K608Clear();
+ do {
+ Gen2PinMsg(password, inData); // transient PIN message
+ ret = Atecc608bKdfNoAuth(accountSlot.rollKdf, inData, 32, x); // first KDF over the PIN message
+ CHECK_ERRCODE_BREAK("kdf kdf_i", ret);
+ ret = Atecc608bKdfNoAuth(SLOT_RESET_KEY, x, 32, inData); // second KDF using slot 13 (in/out must not alias)
+ CHECK_ERRCODE_BREAK("kdf R", ret);
+ sha256((struct sha256 *)piece608, inData, 32); // piece608 = sha256 of the KDF output
+ ret = Gen2DeriveK608(piece608, k608); // stash K608 for on_unlock_success
+ CHECK_ERRCODE_BREAK("k608", ret);
+ Gen2K608Set(k608);
+ } while (0);
+ CLEAR_ARRAY(inData);
+ CLEAR_ARRAY(x);
+ CLEAR_ARRAY(k608);
+ return ret;
+}
+// gen-2 account-existence count. Unlike the shared GetExistAccountNum (which keys only off the IV page),
+// an account counts as valid only when ALL THREE of its signals are present:
+// 1. the lifecycle status page == CREATED, AND
+// 2. a valid IV page (seed blob present), AND
+// 3. a valid R_wrapped page (page 8).
+// Any account missing one of the three (interrupted create, half-erased, status not yet committed) is not
+// counted. Used only to gate fresh-device provisioning in Gen2Provision608.
+static int32_t Gen2GetExistAccountNum(uint8_t *accountNum)
+{
+ int32_t ret = SUCCESS_CODE;
+ uint8_t iv[32], rWrapped[32], count = 0;
+ AccountStatus_t status;
+
+ for (uint8_t i = 0; i < 3; i++) {
+ ret = SE_GetAccountStatus(i, &status);
+ CHECK_ERRCODE_BREAK("read status", ret);
+ if (status != ACCOUNT_STATUS_CREATED) { // 1. status must be CREATED
+ continue;
+ }
+ ret = SE_HmacEncryptRead(iv, i * PAGE_NUM_PER_ACCOUNT + PAGE_INDEX_IV);
+ CHECK_ERRCODE_BREAK("read iv", ret);
+ ret = SE_HmacEncryptRead(rWrapped, i * PAGE_NUM_PER_ACCOUNT + PAGE_INDEX_R_WRAPPED);
+ CHECK_ERRCODE_BREAK("read R_wrapped", ret);
+ if (CheckEntropy(iv, 32) && CheckEntropy(rWrapped, 32)) { // 2. iv AND 3. R_wrapped both present
+ count++;
+ }
+ }
+ CLEAR_ARRAY(iv);
+ CLEAR_ARRAY(rWrapped);
+ if (ret == SUCCESS_CODE) {
+ *accountNum = count;
+ }
+ return ret;
+}
+// gen-2 seed-state consistency check. On a gen-2 device every committed wallet has status CREATED and every
+// in-progress one has CREATING/CHANGING_PIN/DELETING, so the ONE inconsistent combination is: a seed is
+// actually present (IV AND R_wrapped both have entropy) while the lifecycle status page reads UNKNOWN — a
+// desynced / corrupted status page, which Gen2GetExistAccountNum does not count. Sets *inconsistent = true
+// if any of the 3 accounts is in that state. An in-progress account (e.g. the forgotten wallet during a
+// single-wallet forget-pass re-save) is CREATING, not UNKNOWN, so it is never flagged here.
+static int32_t Gen2HasInconsistentSeed(bool *inconsistent)
+{
+ int32_t ret = SUCCESS_CODE;
+ uint8_t iv[32], rWrapped[32];
+ AccountStatus_t status;
+
+ *inconsistent = false;
+ for (uint8_t i = 0; i < 3; i++) {
+ ret = SE_GetAccountStatus(i, &status);
+ CHECK_ERRCODE_BREAK("read status", ret);
+ if (status != ACCOUNT_STATUS_UNKNOWN) { // only UNKNOWN-with-seed is the invalid case
+ continue;
+ }
+ ret = SE_HmacEncryptRead(iv, i * PAGE_NUM_PER_ACCOUNT + PAGE_INDEX_IV);
+ CHECK_ERRCODE_BREAK("read iv", ret);
+ ret = SE_HmacEncryptRead(rWrapped, i * PAGE_NUM_PER_ACCOUNT + PAGE_INDEX_R_WRAPPED);
+ CHECK_ERRCODE_BREAK("read R_wrapped", ret);
+ if (CheckEntropy(iv, 32) && CheckEntropy(rWrapped, 32)) { // seed present but no status -> inconsistent
+ *inconsistent = true;
+ printf("account %d: seed present (iv+R_wrapped) but status UNKNOWN -> inconsistent, refusing R-birth\n", i);
+ break;
+ }
+ }
+ CLEAR_ARRAY(iv);
+ CLEAR_ARRAY(rWrapped);
+ return ret;
+}
+// Create (provision) a gen-2 account. Returns piece608 (mixed into the seed by the caller). Steps:
+// 1. Establish R. `R` is supplied by the caller (add-wallet: recovered from an existing wallet).
+// `R == NULL` means a new device -> generate a fresh R and write slot 13, but only when no other wallet
+// exists; if other wallets exist we refuse rather than write a new R. When R is supplied we never
+// rewrite slot 13.
+// 2. Birth this account's kdf_i (per-account ROLL_KDF slot; no-auth DeriveKey).
+// 3. Derive piece608 the same way login will, which also stashes K608.
+// 4. Store R_wrapped for this account under that K608.
+// 5. Arm the attempt Limit (idempotent — converts the factory bootstrap value to the runtime window).
+// R is local to this frame (no persistent session copy); the post-create login-derive re-recovers it.
+static int32_t Gen2Provision608(uint8_t accountIndex, const char *password, uint8_t piece608[32], const uint8_t *R)
+{
+ uint8_t rKey[32], k608[32];
+ AccountSlot_t accountSlot;
+ uint8_t otherAccounts = 0;
+ int32_t ret;
+
+ ASSERT(accountIndex <= 2);
+ GetAccountSlot(&accountSlot, accountIndex);
+ do {
+ // 1. Establish R. The caller supplies R when it already lives on-chip (add-wallet: recovered from an
+ // existing wallet). R == NULL means "new device" -> generate a fresh R and write slot 13, but only
+ // when no other wallet exists.
+ if (R != NULL) {
+ memcpy(rKey, R, 32); // add-wallet: reuse the live slot-13 R; do NOT rewrite slot 13
+ } else {
+ ret = Gen2GetExistAccountNum(&otherAccounts);
+ CHECK_ERRCODE_BREAK("exist account num", ret);
+ // A seed present with status UNKNOWN (a desynced status page) is NOT counted above
+ // (status != CREATED). Refuse provisioning if any such inconsistent seed exists. The forgotten
+ // wallet in a single-wallet forget-pass re-save is CREATING (not UNKNOWN), so it is never flagged
+ // -> that flow still proceeds.
+ bool inconsistentSeed = false;
+ ret = Gen2HasInconsistentSeed(&inconsistentSeed);
+ CHECK_ERRCODE_BREAK("seed consistency", ret);
+ if (otherAccounts != 0 || inconsistentSeed) {
+ ret = ERR_GENERAL_FAIL; // R already on-chip / inconsistent seed; refuse
+ break;
+ }
+ SE_GetTRng(rKey, 32); // fresh R from TRNG
+ ret = SE_EncryptWrite(SLOT_RESET_KEY, 0, rKey); // slot 13, encrypted write via slot 2
+ CHECK_ERRCODE_BREAK("write R", ret);
+ }
+ // 2. Birth this account's kdf_i (no-auth DeriveKey on the ROLL_KDF slot).
+ ret = Atecc608bDeriveKeyNoAuth(accountSlot.rollKdf);
+ CHECK_ERRCODE_BREAK("derive kdf_i", ret);
+ // 3. Derive piece608 and stash K608.
+ ret = Gen2Derive608(accountIndex, password, piece608);
+ CHECK_ERRCODE_BREAK("derive piece608", ret);
+ ret = Gen2K608Get(k608);
+ CHECK_ERRCODE_BREAK("k608", ret);
+ // 4. Wrap + store R for this account.
+ ret = Gen2StoreRWrapped(accountIndex, rKey, k608);
+ CHECK_ERRCODE_BREAK("store R_wrapped", ret);
+ // 5. Arm the attempt Limit.
+ ret = SE_RearmMatchCount(rKey);
+ CHECK_ERRCODE_BREAK("arm match_count", ret);
+ } while (0);
+ Gen2K608Clear(); // K608 is single-use
+ CLEAR_ARRAY(rKey);
+ CLEAR_ARRAY(k608);
+ return ret;
+}
+// Called once after a verified unlock (correct PIN). Recovers R from this account's R_wrapped using the
+// K608 just stashed by Gen2Derive608, then re-arms the attempt Limit. R lives only on this stack frame;
+// K608 is consumed (one-shot) here.
+static int32_t Gen2OnUnlockSuccess(uint8_t accountIndex)
+{
+ uint8_t R[32];
+ int32_t ret;
+
+ ASSERT(accountIndex <= 2);
+ do {
+ ret = Gen2RecoverR(accountIndex, R); // recover R from page 8
+ CHECK_ERRCODE_BREAK("recover R", ret);
+ ret = SE_RearmMatchCount(R); // re-arm the attempt Limit
+ CHECK_ERRCODE_BREAK("re-arm match_count", ret);
+ } while (0);
+ // K608 is single-use; drop it whether or not re-arm succeeded.
+ Gen2K608Clear();
+ CLEAR_ARRAY(R);
+ return ret;
+}
+// Recover R from an existing wallet so the add-wallet path can provision a new wallet with it (instead of
+// generating a new R). Re-derives the existing wallet's K608 from its passcode, then recovers R from
+// R_wrapped(existingIdx). Used just-in-time by SetNewKeyPieceToSE; the existing wallet was already
+// authenticated at the Add-Wallet verify (its passcode preserved in the cache).
+static int32_t Gen2RecoverResetKey(uint8_t existingIdx, const char *existingPassword, uint8_t *R_out)
+{
+ uint8_t piece[32];
+ int32_t ret;
+
+ ASSERT(existingIdx <= 2);
+ if (existingPassword == NULL || strnlen_s(existingPassword, PASSWORD_MAX_LEN) == 0) {
+ return ERR_GENERAL_FAIL; // no existing-wallet passcode -> cannot recover; caller fails closed
+ }
+ do {
+ ret = Gen2Derive608(existingIdx, existingPassword, piece); // stash existing K608 (piece unused here)
+ CHECK_ERRCODE_BREAK("recover-key derive", ret);
+ ret = Gen2RecoverR(existingIdx, R_out); // recover R from R_wrapped(existingIdx)
+ CHECK_ERRCODE_BREAK("recover-key R", ret);
+ } while (0);
+ Gen2K608Clear(); // existing K608 consumed; provision will re-stash the new wallet's K608
+ CLEAR_ARRAY(piece);
+ return ret;
+}
+static int32_t Gen2PrepareChangePin(uint8_t accountIndex, const char *oldPassword)
+{
+ SE_ArmProvisionRecovery(accountIndex, oldPassword); // PIN_old: recover R just-in-time during re-provision
+ return SUCCESS_CODE;
+}
+// Per-wallet cryptographic erase (forget one wallet, keep the others). The caller (DestroyAccount) has
+// already zeroed this account's DS28S60 pages (seed blob + R_wrapped). Here we additionally roll the
+// account's kdf_i via DeriveKey(Roll) (no PIN) so its piece608 can never be reproduced. R and the other
+// wallets are untouched.
+static int32_t Gen2EraseAccount(uint8_t accountIndex)
+{
+ AccountSlot_t accountSlot;
+ ASSERT(accountIndex <= 2);
+ GetAccountSlot(&accountSlot, accountIndex);
+ return Atecc608bDeriveKeyNoAuth(accountSlot.rollKdf); // roll kdf_i -> this wallet's piece608 dies
+}
+// Full wipe: overwrite R (slot 13) with fresh TRNG, making all seed blobs undecryptable at once. The R write
+// is WriteConfig=Encrypt via slot 2. The caller (WipeDevice) also zeros the DS28S60 blobs + erases flash;
+// this is the SE-side cryptographic kill.
+static int32_t Gen2WipeAll(void)
+{
+ uint8_t newR[32];
+ int32_t ret;
+ SE_GetTRng(newR, 32);
+ ret = SE_EncryptWrite(SLOT_RESET_KEY, 0, newR); // slot 13, encrypted write via slot 2
+ CLEAR_ARRAY(newR);
+ return ret;
+}
+static int32_t Gen2BootMigrate(void)
+{
+ return SUCCESS_CODE; // no gen-2 boot work yet
+}
+// Wipe transient gen-2 session secrets. R is never persisted (it lives only on the stack of the call that
+// uses it), so the only thing to clear is K608 — which spans derive_608 -> on_unlock_success and would
+// otherwise linger if an error aborts the unlock between those two calls.
+static void Gen2ClearSession(void)
+{
+ Gen2K608Clear();
+}
+
+const SeAccountBackend g_seBackendGen2 = {
+ .derive_608 = Gen2Derive608,
+ .provision_608 = Gen2Provision608,
+ .recover_reset_key = Gen2RecoverResetKey,
+ .on_unlock_success = Gen2OnUnlockSuccess,
+ .prepare_change_pin = Gen2PrepareChangePin,
+ .erase_account = Gen2EraseAccount,
+ .wipe_all = Gen2WipeAll,
+ .boot_migrate = Gen2BootMigrate,
+ .clear_session = Gen2ClearSession,
+};
diff --git a/src/managers/se_manager.c b/src/managers/se_manager.c
index b519588..e4629ce 100644
--- a/src/managers/se_manager.c
+++ b/src/managers/se_manager.c
@@ -14,57 +14,14 @@
#include "log_print.h"
#include "hash_and_salt.h"
#include "secret_cache.h"
+#include "se_account_backend.h"
-#define SHA256_COUNT 3
-
-static int32_t SetNewKeyPieceToAtecc608b(uint8_t accountIndex, uint8_t *piece, const char *password);
static int32_t SetNewKeyPieceToDs28s60(uint8_t accountIndex, uint8_t *piece, const char *password);
-static int32_t GetKeyPieceFromAtecc608b(uint8_t accountIndex, uint8_t *piece, const char *password);
static int32_t GetKeyPieceFromDs28s60(uint8_t accountIndex, uint8_t *piece, const char *password);
-static int32_t NormalizeAteccAuthError(int32_t ret)
-{
- return (ret == ATCA_CHECKMAC_VERIFY_FAILED) ? ERR_KEYSTORE_AUTH : ret;
-}
-
-static int32_t SetNewKeyPieceToAtecc608b(uint8_t accountIndex, uint8_t *piece, const char *password)
-{
- uint8_t authKey[32], hostRandom[32], inData[32], outData[32];
- int32_t ret;
- AccountSlot_t accountSlot;
-
- ASSERT(accountIndex <= 2);
- do {
- HashWithSalt(authKey, (uint8_t *)password, strnlen_s(password, PASSWORD_MAX_LEN), "auth_key");
- GetAccountSlot(&accountSlot, accountIndex);
- ret = SE_EncryptWrite(accountSlot.auth, 0, authKey);
- CHECK_ERRCODE_BREAK("write auth", ret);
- ret = SE_DeriveKey(accountSlot.rollKdf, authKey);
- CHECK_ERRCODE_BREAK("derive key", ret);
- TrngGet(hostRandom, 32);
- ret = SE_EncryptWrite(accountSlot.hostKdf, 0, hostRandom);
- CHECK_ERRCODE_BREAK("write kdf", ret);
-
- HashWithSalt(outData, (uint8_t *)password, strnlen_s(password, PASSWORD_MAX_LEN), "password_atecc608b");
- memcpy(inData, outData, 32);
- ret = SE_Kdf(accountSlot.rollKdf, authKey, inData, 32, outData);
- CHECK_ERRCODE_BREAK("kdf", ret);
- memcpy(inData, outData, 32);
- ret = SE_Kdf(accountSlot.hostKdf, authKey, inData, 32, outData);
- CHECK_ERRCODE_BREAK("kdf", ret);
- for (uint32_t i = 0; i < SHA256_COUNT; i++) {
- memcpy(inData, outData, 32);
- sha256((struct sha256 *)outData, inData, 32);
- }
- memcpy(piece, outData, 32);
- } while (0);
- CLEAR_ARRAY(authKey);
- CLEAR_ARRAY(hostRandom);
- CLEAR_ARRAY(inData);
- CLEAR_ARRAY(outData);
-
- return ret;
-}
+// gen-1-specific 608 derivation lives in se_backend_gen1.c; gen-2 in se_backend_gen2.c. This file keeps
+// the generation-agnostic pieces: the shared DS28S60 key piece, GetAccountSlot, the SeBackend() dispatcher
+// and SE_* helpers.
static int32_t SetNewKeyPieceToDs28s60(uint8_t accountIndex, uint8_t *piece, const char *password)
{
@@ -89,39 +46,6 @@ static int32_t SetNewKeyPieceToDs28s60(uint8_t accountIndex, uint8_t *piece, con
return ret;
}
-static int32_t GetKeyPieceFromAtecc608b(uint8_t accountIndex, uint8_t *piece, const char *password)
-{
- uint8_t authKey[32], inData[32], outData[32];
- int32_t ret;
- AccountSlot_t accountSlot;
-
- ASSERT(accountIndex <= 2);
- do {
- HashWithSalt(authKey, (uint8_t *)password, strnlen_s(password, PASSWORD_MAX_LEN), "auth_key");
- HashWithSalt(outData, (uint8_t *)password, strnlen_s(password, PASSWORD_MAX_LEN), "password_atecc608b");
- memcpy(inData, outData, 32);
-
- GetAccountSlot(&accountSlot, accountIndex);
- ret = SE_Kdf(accountSlot.rollKdf, authKey, inData, 32, outData);
- ret = NormalizeAteccAuthError(ret);
- CHECK_ERRCODE_BREAK("kdf", ret);
- memcpy(inData, outData, 32);
- ret = SE_Kdf(accountSlot.hostKdf, authKey, inData, 32, outData);
- ret = NormalizeAteccAuthError(ret);
- CHECK_ERRCODE_BREAK("kdf", ret);
- for (uint32_t i = 0; i < SHA256_COUNT; i++) {
- memcpy(inData, outData, 32);
- sha256((struct sha256 *)outData, inData, 32);
- }
- memcpy(piece, outData, 32);
- } while (0);
- CLEAR_ARRAY(authKey);
- CLEAR_ARRAY(inData);
- CLEAR_ARRAY(outData);
-
- return ret;
-}
-
static int32_t GetKeyPieceFromDs28s60(uint8_t accountIndex, uint8_t *piece, const char *password)
{
uint8_t passwordHash[32], xData[32];
@@ -172,9 +96,14 @@ void GetAccountSlot(AccountSlot_t *accountSlot, uint8_t accountIndex)
int32_t GetKeyPieceFromSE(uint8_t accountIndex, uint8_t *pieces, const char *password)
{
int32_t ret;
+ const SeAccountBackend *be = SeBackend();
+ if (be == NULL) {
+ return ERR_GENERAL_FAIL; // fail closed: UNPROVISIONED / INVALID must never derive
+ }
do {
- ret = GetKeyPieceFromAtecc608b(accountIndex, pieces, password);
+ // 608 piece is generation-specific; DS28S60 piece is shared.
+ ret = be->derive_608(accountIndex, password, pieces);
CHECK_ERRCODE_BREAK("atecc piece", ret);
// KEYSTORE_PRINT_ARRAY("608 piece", pieces, 32);
ret = GetKeyPieceFromDs28s60(accountIndex, pieces + KEY_PIECE_LEN, password);
@@ -185,11 +114,75 @@ int32_t GetKeyPieceFromSE(uint8_t accountIndex, uint8_t *pieces, const char *pas
return ret;
}
+// Add-wallet provisioning context. Holds the existing wallet's index + passcode so the gen-2 backend can
+// reuse that wallet's existing SE key material during SetNewKeyPieceToSE. This is a DEDICATED holder with its
+// own lifecycle (NOT the shared secret cache, which the add-wallet navigation clears before provision runs):
+// arm = Add-Wallet passcode verify (captures idx + passcode while both are valid)
+// clear = consumed by the next provision (one-shot) OR logout OR lock-screen turn-on.
+// So the passcode never outlives the one add-wallet operation / the unlocked session. 0xFF = not armed.
+#define PROVISION_RECOVER_IDX_NONE 0xFF
+static uint8_t g_provisionRecoverIdx = PROVISION_RECOVER_IDX_NONE;
+static char g_provisionRecoverPassword[PASSWORD_MAX_LEN + 1] = {0};
+
+void SE_ArmProvisionRecovery(uint8_t existingIndex, const char *existingPassword)
+{
+ g_provisionRecoverIdx = existingIndex;
+ memset_s(g_provisionRecoverPassword, sizeof(g_provisionRecoverPassword), 0, sizeof(g_provisionRecoverPassword));
+ if (existingPassword != NULL) {
+ strcpy_s(g_provisionRecoverPassword, sizeof(g_provisionRecoverPassword), existingPassword);
+ }
+}
+
+void SE_DisarmProvisionRecovery(void)
+{
+ g_provisionRecoverIdx = PROVISION_RECOVER_IDX_NONE;
+ memset_s(g_provisionRecoverPassword, sizeof(g_provisionRecoverPassword), 0, sizeof(g_provisionRecoverPassword));
+}
+
+// True while an add-wallet / forget-pass provision recovery is armed (pending consume). Lets a re-openable
+// sub-view avoid clobbering the page-lock hold that the operation ROOT owns for the whole armed interval
+// (page-lock is a plain bool, not refcounted). Always false in gen-1 / simulator (never armed).
+bool SE_IsProvisionRecoveryArmed(void)
+{
+ return g_provisionRecoverIdx != PROVISION_RECOVER_IDX_NONE;
+}
+
+// Semantic alias for the UI: call this at an add-wallet / forget-pass OPERATION BOUNDARY when the user leaves
+// the flow without provisioning (notice-tile teardown, forget-pass DeInit, backing past the proven step). It
+// makes the intent explicit at the call site instead of reading like incidental view-teardown cleanup. Must
+// only be used at the operation root boundary — never in a re-openable sub-step (e.g. the create-wallet set-pin
+// view), or the arm is dropped mid-flow and the eventual provision fails recovery. The consume path
+// (SetNewKeyPieceToSE), the change-PIN in-function disarm, and the lock-screen session catch-all keep calling
+// SE_DisarmProvisionRecovery() directly — those are not "user abandoned the operation" boundaries.
+void AbandonProvisionRecovery(void)
+{
+ SE_DisarmProvisionRecovery();
+}
+
int32_t SetNewKeyPieceToSE(uint8_t accountIndex, uint8_t *pieces, const char *password)
{
//TODO: deal with error code
int32_t ret;
- ret = SetNewKeyPieceToAtecc608b(accountIndex, pieces, password);
+ const SeAccountBackend *be = SeBackend();
+ uint8_t rBuf[32];
+ const uint8_t *R = NULL;
+
+ if (be == NULL) {
+ return ERR_GENERAL_FAIL; // fail closed
+ }
+ // Add-wallet: an existing wallet was authenticated at the Add-Wallet verify (its index armed here, its
+ // passcode preserved in the secret cache). Recover its existing SE key material so provision REUSES it
+ // instead of creating new. First wallet / gen-1 stays armed-NONE -> R stays NULL. If recovery fails
+ // (e.g. passcode no longer cached) R stays NULL and the gen-2 backend refuses.
+ if (g_provisionRecoverIdx != PROVISION_RECOVER_IDX_NONE) {
+ int32_t rr = be->recover_reset_key(g_provisionRecoverIdx, g_provisionRecoverPassword, rBuf);
+ if (rr == SUCCESS_CODE) {
+ R = rBuf;
+ }
+ SE_DisarmProvisionRecovery(); // consume (one-shot): clear index + passcode holder
+ }
+ ret = be->provision_608(accountIndex, password, pieces, R);
+ CLEAR_ARRAY(rBuf);
CHECK_ERRCODE_RETURN_INT(ret);
// KEYSTORE_PRINT_ARRAY("608 piece", pieces, 32);
@@ -199,6 +192,117 @@ int32_t SetNewKeyPieceToSE(uint8_t accountIndex, uint8_t *pieces, const char *pa
return ret;
}
+// Refresh the per-account SE state after a verified unlock. Called by the keystore once the account HMAC has
+// confirmed the PIN (gen-2: refreshes SE session state; gen-1: no-op). Fail-closed on NULL backend.
+int32_t SE_OnUnlockSuccess(uint8_t accountIndex)
+{
+ const SeAccountBackend *be = SeBackend();
+
+ if (be == NULL) {
+ return ERR_GENERAL_FAIL;
+ }
+ return be->on_unlock_success(accountIndex);
+}
+
+// Prepare the generation-specific SE state for change-PIN after PIN_old has been verified by LoadAccountSecret.
+// gen-1: no-op. gen-2: arm recovery so SaveAccountSecret's provision_608 reuses the existing SE key material.
+int32_t SE_PrepareChangePin(uint8_t accountIndex, const char *oldPassword)
+{
+ const SeAccountBackend *be = SeBackend();
+
+ if (be == NULL) {
+#ifdef COMPILE_SIMULATOR
+ return SUCCESS_CODE;
+#else
+ return ERR_GENERAL_FAIL;
+#endif
+ }
+ return be->prepare_change_pin(accountIndex, oldPassword);
+}
+
+// Per-wallet SE-side cryptographic erase, called after DestroyAccount has zeroed the account's pages
+// (gen-2: rotates the account's SE key material; gen-1: no-op). Simulator has no SE backend.
+int32_t SE_EraseAccount(uint8_t accountIndex)
+{
+ const SeAccountBackend *be = SeBackend();
+
+ if (be == NULL) {
+#ifdef COMPILE_SIMULATOR
+ return SUCCESS_CODE;
+#else
+ return ERR_GENERAL_FAIL;
+#endif
+ }
+ return be->erase_account(accountIndex);
+}
+
+// Full SE-side cryptographic wipe, called from WipeDevice (gen-2: overwrites the shared SE key material;
+// gen-1: no-op). Fail-closed on NULL backend.
+int32_t SE_WipeAll(void)
+{
+ const SeAccountBackend *be = SeBackend();
+
+ if (be == NULL) {
+ return ERR_GENERAL_FAIL;
+ }
+ return be->wipe_all();
+}
+
+// One-time boot migration, dispatched to the generation backend (called from AccountManagerInit).
+// gen-1: wipe the legacy page-8 PIN hash. gen-2: no-op (page 8 is used differently).
+// Unlike the other dispatchers, a NULL backend (UNPROVISIONED/INVALID — should never reach here; SeManagerInit
+// asserts gen-1/gen-2 at boot) returns SUCCESS = SKIP, not ERR: a boot cleanup must never fail boot or touch
+// page 8 on an unknown generation.
+int32_t SE_BootMigrate(void)
+{
+ const SeAccountBackend *be = SeBackend();
+
+ if (be == NULL) {
+ return SUCCESS_CODE; // fail-closed = skip the wipe (never fail boot / never touch page 8)
+ }
+ return be->boot_migrate();
+}
+
+// Write the per-account lifecycle status page (magic + state byte). state == ACCOUNT_STATUS_UNKNOWN writes an
+// all-zero page (clears the status -> reads back as UNKNOWN -> coarse fallback). Single page write = atomic.
+int32_t SE_SetAccountStatus(uint8_t accountIndex, AccountStatus_t state)
+{
+ uint8_t page[32] = {0};
+ int32_t ret;
+
+ ASSERT(accountIndex <= 2);
+ if (state != ACCOUNT_STATUS_UNKNOWN) {
+ uint32_t magic = ACCOUNT_STATUS_MAGIC;
+ memcpy(page, &magic, sizeof(magic)); // bytes 0-3 = magic
+ page[4] = (uint8_t)state; // byte 4 = state
+ }
+ ret = SE_HmacEncryptWrite(page, PAGE_ACCOUNT_STATUS_BASE + accountIndex);
+ CLEAR_ARRAY(page);
+ return ret;
+}
+
+// Read the status page. *state = ACCOUNT_STATUS_UNKNOWN when the magic is absent (blank / legacy / wiped /
+// read error) so the caller falls back to the coarse boot check.
+int32_t SE_GetAccountStatus(uint8_t accountIndex, AccountStatus_t *state)
+{
+ uint8_t page[32];
+ uint32_t magic;
+ int32_t ret;
+
+ ASSERT(accountIndex <= 2);
+ *state = ACCOUNT_STATUS_UNKNOWN;
+ ret = SE_HmacEncryptRead(page, PAGE_ACCOUNT_STATUS_BASE + accountIndex);
+ if (ret != SUCCESS_CODE) {
+ return ret;
+ }
+ memcpy(&magic, page, sizeof(magic));
+ if (magic == ACCOUNT_STATUS_MAGIC && page[4] >= ACCOUNT_STATUS_CREATING && page[4] <= ACCOUNT_STATUS_DELETING) {
+ *state = (AccountStatus_t)page[4];
+ }
+ CLEAR_ARRAY(page);
+ return SUCCESS_CODE;
+}
+
/// @brief Set the fingerprint encrypted password, store in SE.
/// @param[in] index
/// @param[in] encryptedPassword 32 bytes.
@@ -393,3 +497,139 @@ int32_t GetDevicePublicKey(uint8_t *pubkey)
} while (0);
return ret;
}
+
+// SE generation, resolved ONCE at SeManagerInit() (the locked config is immutable per device, so it is
+// deterministic). All callers read this cached value via GetSeGen() — no per-call chip access.
+// SE_GEN_UNKNOWN (0) = not yet resolved.
+static SeGen_t g_seGen = SE_GEN_UNKNOWN;
+
+#ifndef COMPILE_SIMULATOR
+/// @brief Resolve the SE generation from the locked ATECC608B config manifest. Verifies the FULL pinned
+/// manifest so a partial/mis-provisioned chip is never mistaken for a valid generation. Internal —
+/// run once from SeManagerInit().
+static SeGen_t ResolveSeGen(void)
+{
+ uint8_t cfg[128];
+ bool cfgLocked = false, dataLocked = false;
+
+ if (atcab_read_config_zone(cfg) != ATCA_SUCCESS) {
+ return SE_GEN_INVALID; // cannot read config -> fail closed
+ }
+ atcab_is_locked(LOCK_ZONE_CONFIG, &cfgLocked);
+ atcab_is_locked(LOCK_ZONE_DATA, &dataLocked);
+
+ if (!cfgLocked || !dataLocked) {
+ return SE_GEN_UNPROVISIONED; // blank / partially provisioned
+ }
+
+ // ATECC608 config-zone field offsets: countMatch=byte18, slotConfig[s]=20+2s,
+ // keyConfig[s]=96+2s, chipOptions=bytes 90-91 (little-endian).
+ uint8_t countMatch = cfg[18];
+ uint16_t chipOptions = (uint16_t)cfg[90] | ((uint16_t)cfg[91] << 8);
+#define SE_SC(s) ((uint16_t)cfg[20 + (s) * 2] | ((uint16_t)cfg[20 + (s) * 2 + 1] << 8))
+#define SE_KC(s) ((uint16_t)cfg[96 + (s) * 2] | ((uint16_t)cfg[96 + (s) * 2 + 1] << 8))
+
+ bool gen2Manifest =
+ (countMatch == 0x81) && (chipOptions == 0x0402) &&
+ (SE_SC(8) == 0x4D00) && (SE_SC(13) == 0x42A0) &&
+ (SE_KC(4) == 0x005C) && (SE_KC(7) == 0x005C) && (SE_KC(11) == 0x005C) &&
+ (SE_KC(8) == 0x001C) && (SE_KC(13) == 0x005C);
+
+ bool gen1Manifest =
+ (countMatch == 0x00) && (chipOptions == 0x0402) &&
+ (SE_SC(8) == 0x42C2) && (SE_SC(13) == 0x42C2);
+#undef SE_SC
+#undef SE_KC
+
+ // Generation is decided by the IMMUTABLE locked config manifest ONLY — never by a mutable data value.
+ if (gen2Manifest) {
+ return SE_GEN_2;
+ } else if (gen1Manifest) {
+ return SE_GEN_1;
+ }
+ return SE_GEN_INVALID;
+}
+#endif
+
+/// @brief Resolve and cache the SE generation. Call ONCE at boot, after Atecc608bInit() and before any
+/// SE-account use (GetKeyPieceFromSE / AccountManagerInit / SeBackend()). Idempotent.
+void SeManagerInit(void)
+{
+#ifdef COMPILE_SIMULATOR
+ g_seGen = SE_GEN_1;
+#else
+ g_seGen = ResolveSeGen();
+#endif
+ printf("SeManagerInit: GetSeGen=%d (1=gen1 2=gen2 3=unprovisioned 4=invalid)\r\n", (int)g_seGen);
+ // A shipped device is ALWAYS provisioned -> must be gen-1 or gen-2. SE_GEN_UNKNOWN/UNPROVISIONED/INVALID
+ // here means a read failure, blank, mis-provisioned, or tampered SE — must never happen on a real device.
+ ASSERT(g_seGen == SE_GEN_1 || g_seGen == SE_GEN_2);
+
+ // gen-2 boot brick check: if the SE's counter shows the account can no longer derive, fail the device
+ // cleanly here at boot rather than asserting mid-unlock. Guarded by the read return codes so a transient
+ // SE read glitch can never false-brick a healthy device. (On a fresh/healthy unit this is a no-op.)
+ // Not in the simulator: gen-2 is forced off above, and SE_GetCounter/SE_GetMatchCount live in the
+ // chip-only se_backend_gen2.c which the simulator doesn't compile.
+#ifndef COMPILE_SIMULATOR
+ if (g_seGen == SE_GEN_2) {
+ uint32_t counter = 0, matchCount = 0;
+ int32_t rc = SE_GetCounter(&counter);
+ int32_t rm = SE_GetMatchCount(&matchCount);
+ if (rc == SUCCESS_CODE && rm == SUCCESS_CODE) {
+ ASSERT(counter < matchCount); // exhausted -> halt the device
+ }
+ }
+#endif
+}
+
+/// @brief SE generation (resolved at SeManagerInit). Deterministic; UNPROVISIONED/INVALID mean a
+/// manufacturing defect or tamper — callers must fail closed. Defensive lazy resolve: if a caller
+/// reaches here before SeManagerInit() ran (g_seGen still SE_GEN_UNKNOWN), resolve on the spot so
+/// the value is never the 0 sentinel.
+/// @return SeGen_t (SE_GEN_1 / SE_GEN_2 / SE_GEN_UNPROVISIONED / SE_GEN_INVALID)
+SeGen_t GetSeGen(void)
+{
+#ifdef COMPILE_SIMULATOR
+ return SE_GEN_1;
+#else
+ if (g_seGen == SE_GEN_UNKNOWN) {
+ g_seGen = ResolveSeGen();
+ }
+ ASSERT(g_seGen == SE_GEN_1 || g_seGen == SE_GEN_2);
+ return g_seGen;
+#endif
+}
+
+// ============================================================================
+// Backend dispatcher. The gen-1 and gen-2 backends + all their logic are isolated in
+// se_backend_gen1.c / se_backend_gen2.c; SeBackend() selects between them from GetSeGen().
+// ============================================================================
+
+const SeAccountBackend *SeBackend(void)
+{
+#ifdef COMPILE_SIMULATOR
+ // The simulator routes keystore ops through SimulatorXxx stubs and never compiles the (chip-dependent)
+ // gen-1/gen-2 backend files, so there is no real backend to dispatch to. Return NULL — every SeBackend()
+ // caller already fails closed on NULL, and those paths aren't exercised under COMPILE_SIMULATOR anyway.
+ return NULL;
+#else
+ switch (GetSeGen()) {
+ case SE_GEN_1:
+ return &g_seBackendGen1;
+ case SE_GEN_2:
+ return &g_seBackendGen2;
+ default:
+ return NULL; // UNPROVISIONED / INVALID -> fail closed (callers must handle)
+ }
+#endif
+}
+
+// Wipe SE-side transient session secrets for the active generation. Safe to call anytime (NULL backend ->
+// no-op). Wired into ClearSecretCache() so SE session material dies with the passcode cache.
+void SE_ClearSessionSecrets(void)
+{
+ const SeAccountBackend *be = SeBackend();
+ if (be != NULL && be->clear_session != NULL) {
+ be->clear_session();
+ }
+}
diff --git a/src/managers/se_manager.h b/src/managers/se_manager.h
index a063240..60b1fa8 100644
--- a/src/managers/se_manager.h
+++ b/src/managers/se_manager.h
@@ -19,8 +19,34 @@
#define PAGE_INDEX_KEY_PIECE 7
// Don't use this page in the future usage, it is only for legacy password hash
#define PAGE_INDEX_LEGACY_PASSWORD_HASH 8
+// gen-2: page 8 is repurposed for per-account gen-2 data (same offset; gen-1 never touches it).
+#define PAGE_INDEX_R_WRAPPED 8
#define PAGE_INDEX_PARAM 9
#define PAGE_INDEX_MULTISIG_CONFIG_HASH 10
+// (per-account index 11 is unused)
+
+// Per-account lifecycle status — ABSOLUTE pages (NOT the per-account *PAGE_NUM_PER_ACCOUNT scheme), placed
+// in the free 36~71 gap. Kept OUTSIDE each account's 0~11 data block so the status survives the
+// DestroyAccount / AccountsDataCheck zero-loops (those loops stay unchanged). One page per account:
+// account i status page = PAGE_ACCOUNT_STATUS_BASE + i (i = 0..2).
+#define PAGE_ACCOUNT_STATUS_BASE 36 // account 0 status; account i = BASE + i
+#define PAGE_ACCOUNT_STATUS_0 36
+#define PAGE_ACCOUNT_STATUS_1 37
+#define PAGE_ACCOUNT_STATUS_2 38
+
+// Status page layout: a 4-byte magic + a 1-byte state (rest zero). The magic lets a blank/legacy/wiped page
+// (all-zero, or random) be told apart from a real state — without it we fall back to the coarse CheckEntropy
+// boot check, so existing accounts keep working. A bootloader/firmware wipe zeros the page -> magic absent
+// -> UNKNOWN. Every mutating op brackets its writes: in-progress state FIRST, terminal state LAST.
+#define ACCOUNT_STATUS_MAGIC 0x4B335354u // 'K''3''S''T'
+typedef enum {
+ ACCOUNT_STATUS_UNKNOWN = 0, // no valid magic (blank / legacy / wiped) -> caller does coarse check
+ ACCOUNT_STATUS_CREATING = 1, // provision in progress -> boot: erase the partial account
+ ACCOUNT_STATUS_CREATED = 2, // valid, complete account
+ ACCOUNT_STATUS_CHANGING_PIN = 3, // change-PIN re-wrap in progress -> boot: erase (user restores seed)
+ ACCOUNT_STATUS_DELETING = 4, // delete in progress -> boot: erase (finish the deletion)
+} AccountStatus_t;
+
//page 76~85 encrypted password
#define PAGE_PF_ENCRYPTED_PASSWORD 72
#define PAGE_PF_AES_KEY 82
@@ -32,6 +58,17 @@
#define PAGE_PUBLIC_INFO 88
+// SE generation, resolved once at runtime from the locked ATECC608B config.
+// A shipped device is pre-provisioned, so a normal boot is SE_GEN_1 or SE_GEN_2;
+// UNPROVISIONED / INVALID are treated as errors.
+typedef enum {
+ SE_GEN_UNKNOWN = 0, // not yet resolved (initial g_seGen value before SeManagerInit / lazy resolve)
+ SE_GEN_1 = 1, // gen-1: fielded / current-locked config
+ SE_GEN_2 = 2, // gen-2: new production
+ SE_GEN_UNPROVISIONED, // blank / config or data zone not locked
+ SE_GEN_INVALID, // both zones locked but the manifest matches neither generation
+} SeGen_t;
+
typedef struct {
uint8_t auth;
uint8_t rollKdf;
@@ -40,6 +77,15 @@ typedef struct {
int32_t GetKeyPieceFromSE(uint8_t accountIndex, uint8_t *piece, const char *password);
int32_t SetNewKeyPieceToSE(uint8_t accountIndex, uint8_t *piece, const char *password);
+// Add-wallet (gen-2): arm at the Add-Wallet passcode verify with the existing wallet's index + passcode,
+// captured into a dedicated holder that the next SetNewKeyPieceToSE consumes. Cleared
+// when consumed by provision (one-shot), on logout, and on lock-screen turn-on.
+void SE_ArmProvisionRecovery(uint8_t existingIndex, const char *existingPassword);
+void SE_DisarmProvisionRecovery(void);
+// UI operation-boundary alias for SE_DisarmProvisionRecovery() (add-wallet notice / forget-pass abandon).
+void AbandonProvisionRecovery(void);
+// True while a provision-recovery is armed (pending consume); used to avoid re-enabling page-lock mid-operation.
+bool SE_IsProvisionRecoveryArmed(void);
int32_t SetFpEncryptedPassword(uint32_t index, const uint8_t *encryptedPassword);
int32_t SetFpStateInfo(uint8_t *info);
int32_t GetFpStateInfo(uint8_t *info);
@@ -50,6 +96,25 @@ int32_t SetFpResetKey(const uint8_t *resetKey);
int32_t GetFpResetKey(uint8_t *resetKey);
bool FpAesKeyExist();
void GetAccountSlot(AccountSlot_t *accountSlot, uint8_t accountIndex);
+void SeManagerInit(void); // resolve SE generation once at boot (after Atecc608bInit)
+SeGen_t GetSeGen(void); // cached value resolved by SeManagerInit
+
+// gen-2 attempt-limit (match_count) helpers.
+int32_t SE_GetCounter(uint32_t *counter); // SE monotonic counter
+int32_t SE_GetMatchCount(uint32_t *matchCount); // reads match_count (slot 8)
+int32_t SE_SetMatchCount(uint32_t matchCount, const uint8_t *R);// encrypted-write slot 8
+int32_t SE_RearmMatchCount(const uint8_t *R); // re-arm match_count; no counter bump
+void SE_ClearSessionSecrets(void); // wipe SE-side transient session secrets (gen-2)
+int32_t SE_OnUnlockSuccess(uint8_t accountIndex); // post-verify re-arm (gen-2); no-op (gen-1)
+int32_t SE_PrepareChangePin(uint8_t accountIndex, const char *oldPassword); // pre-reprovision hook: no-op (gen-1); arm provision recovery (gen-2)
+int32_t SE_EraseAccount(uint8_t accountIndex); // per-wallet SE erase (gen-2); no-op (gen-1)
+int32_t SE_WipeAll(void); // full SE wipe (gen-2); no-op (gen-1)
+int32_t SE_BootMigrate(void); // one-time boot: legacy page-8 wipe (gen-1); no-op (gen-2); NULL->skip
+// Per-account lifecycle status page (PAGE_ACCOUNT_STATUS_BASE + idx). SetAccountStatus(UNKNOWN) zeros it.
+// GetAccountStatus returns UNKNOWN when the magic is absent (blank/legacy) so the caller falls back to the
+// coarse boot check. Single-page write/read -> each transition is atomic.
+int32_t SE_SetAccountStatus(uint8_t accountIndex, AccountStatus_t state);
+int32_t SE_GetAccountStatus(uint8_t accountIndex, AccountStatus_t *state);
int32_t SignMessageWithDeviceKey(uint8_t *messageHash, uint8_t *signaure);
int32_t GetDevicePublicKey(uint8_t *pubkey);
int32_t SetWalletDataHash(uint8_t index, uint8_t *info);
diff --git a/src/ui/gui_components/gui_keyboard_hintbox.c b/src/ui/gui_components/gui_keyboard_hintbox.c
index 82fca69..ca74ead 100644
--- a/src/ui/gui_components/gui_keyboard_hintbox.c
+++ b/src/ui/gui_components/gui_keyboard_hintbox.c
@@ -17,6 +17,7 @@
#include "fingerprint_process.h"
#include "gui_model.h"
#include "usb_task.h"
+#include "gui_wipe_device_widgets.h"
#ifndef COMPILE_SIMULATOR
#include "usb_task.h"
@@ -89,6 +90,8 @@ static KeyboardWidget_t *CreateKeyboardWidget()
}
keyboardWidget->kb = NULL;
keyboardWidget->errLabel = NULL;
+ keyboardWidget->titleLabel = NULL;
+ keyboardWidget->noticeLabel = NULL;
static uint16_t sig = ENTER_PASSCODE_VERIFY_PASSWORD;
keyboardWidget->sig = &sig;
keyboardWidget->countDownTimer = NULL;
@@ -110,6 +113,21 @@ void SetKeyboardWidgetSelf(KeyboardWidget_t *keyboardWidget, KeyboardWidget_t **
keyboardWidget->self = self;
}
+// Override the default title/desc (used e.g. by the forget-pass "Prove Device Ownership" step). Pass NULL to
+// leave a field unchanged.
+void SetKeyboardWidgetTitle(KeyboardWidget_t *keyboardWidget, const char *title, const char *desc)
+{
+ if (keyboardWidget == NULL) {
+ return;
+ }
+ if (title != NULL && keyboardWidget->titleLabel != NULL) {
+ lv_label_set_text(keyboardWidget->titleLabel, title);
+ }
+ if (desc != NULL && keyboardWidget->noticeLabel != NULL) {
+ lv_label_set_text(keyboardWidget->noticeLabel, desc);
+ }
+}
+
static void ClearKeyboardWidgetCache(KeyboardWidget_t *keyboardWidget)
{
memset_s(g_pinBuf, sizeof(g_pinBuf), 0, sizeof(g_pinBuf));
@@ -238,11 +256,13 @@ KeyboardWidget_t *GuiCreateKeyboardWidgetView(lv_obj_t *parent, lv_event_cb_t bu
lv_obj_t *label = GuiCreateScrollTitleLabel(keyboardHintBox, _("change_passcode_mid_btn"));
lv_obj_align(label, LV_ALIGN_DEFAULT, 36, 12 + GUI_NAV_BAR_HEIGHT);
+ keyboardWidget->titleLabel = label;
label = GuiCreateNoticeLabel(keyboardHintBox, _("passphrase_add_password"));
if (*signal == SIG_FINGER_REGISTER_ADD_SUCCESS) {
lv_label_set_text(label, _("fingerprint_add_password"));
}
lv_obj_align_to(label, lv_obj_get_child(keyboardHintBox, lv_obj_get_child_cnt(keyboardHintBox) - 2), LV_ALIGN_OUT_BOTTOM_LEFT, 0, 12);
+ keyboardWidget->noticeLabel = label;
keyboardWidget->keyboardHintBox = keyboardHintBox;
KeyBoard_t *kb = GuiCreateFullKeyBoard(keyboardHintBox, KeyboardConfirmHandler, KEY_STONE_FULL_L, keyboardWidget);
@@ -264,7 +284,7 @@ KeyboardWidget_t *GuiCreateKeyboardWidgetView(lv_obj_t *parent, lv_event_cb_t bu
lv_obj_add_event_cb(img, SwitchPasswordModeHandler, LV_EVENT_CLICKED, ta);
keyboardWidget->eyeImg = img;
- if (*signal != SIG_FINGER_REGISTER_ADD_SUCCESS) {
+ if (*signal != SIG_FINGER_REGISTER_ADD_SUCCESS && *signal != SIG_FORGET_PASSWORD_PROVE_OWNERSHIP) {
button = GuiCreateImgLabelAdaptButton(keyboardHintBox, _("FORGET"), &imgLock, ForgetHandler, NULL);
lv_obj_align(button, LV_ALIGN_TOP_RIGHT, -24, 439 - GUI_STATUS_BAR_HEIGHT);
}
@@ -429,9 +449,21 @@ void GuiShowErrorNumber(KeyboardWidget_t *keyboardWidget, PasswordVerifyResult_t
memset_s(g_pinBuf, sizeof(g_pinBuf), 0, sizeof(g_pinBuf));
keyboardWidget->currentNum = 0;
printf("GuiShowErrorNumber error count is %d\n", passwordVerifyResult->errorCount);
+
+ // Forget-pass prove-ownership counts toward loginPasswordErrorCount (cap MAX_LOGIN) and, at the
+ // cap, opens the existing wipe-device view. Every other caller uses the settings device-lock cap
+ // (MAX_CURRENT_PASSWORD_ERROR_COUNT_SHOW_HINTBOX) and the in-modal device-lock hintbox.
+ uint16_t signal = passwordVerifyResult->signal != NULL ? *(uint16_t *)passwordVerifyResult->signal : 0;
+ bool proveOwnership = (signal == SIG_FORGET_PASSWORD_PROVE_OWNERSHIP);
+ uint8_t maxCount = proveOwnership ? MAX_LOGIN_PASSWORD_ERROR_COUNT
+ : MAX_CURRENT_PASSWORD_ERROR_COUNT_SHOW_HINTBOX;
+ printf("GuiShowErrorNumber signal=%u proveOwnership=%d maxCount=%u lock=%d forget=%d wipe=%d\r\n",
+ (unsigned int)signal, proveOwnership, (unsigned int)maxCount, g_lockView.isActive, g_forgetPassView.isActive,
+ g_wipeDeviceView.isActive);
+
char hint[BUFFER_SIZE_128];
char tempBuf[BUFFER_SIZE_128];
- uint8_t cnt = MAX_CURRENT_PASSWORD_ERROR_COUNT_SHOW_HINTBOX - passwordVerifyResult->errorCount;
+ uint8_t cnt = maxCount - passwordVerifyResult->errorCount;
if (cnt > 1) {
snprintf_s(hint, BUFFER_SIZE_128, _("unlock_device_attempts_left_plural_times_fmt"), cnt);
} else {
@@ -439,9 +471,30 @@ void GuiShowErrorNumber(KeyboardWidget_t *keyboardWidget, PasswordVerifyResult_t
}
snprintf_s(tempBuf, BUFFER_SIZE_128, "#F55831 %s#", hint);
GuiSetErrorLabel(keyboardWidget, tempBuf);
- if (passwordVerifyResult->errorCount == MAX_CURRENT_PASSWORD_ERROR_COUNT_SHOW_HINTBOX) {
+ if (passwordVerifyResult->errorCount == maxCount) {
CloseUsb();
- GuiShowPasswordErrorHintBox(keyboardWidget);
+ printf("GuiShowErrorNumber max reached signal=%u proveOwnership=%d errorCount=%u\r\n",
+ (unsigned int)signal, proveOwnership, (unsigned int)passwordVerifyResult->errorCount);
+ if (proveOwnership) {
+ // Tear down the modal first (self-pointer NULLs the caller's handle), then unwind the
+ // forget-pass flow and open the existing wipe-device page on top of the lock screen.
+ printf("prove-ownership wipe transition: delete keyboard lock=%d forget=%d wipe=%d\r\n",
+ g_lockView.isActive, g_forgetPassView.isActive, g_wipeDeviceView.isActive);
+ GuiDeleteKeyboardWidget(keyboardWidget);
+ if (GuiCheckIfViewOpened(&g_lockView)) {
+ int32_t closeRet = GuiCloseToTargetView(&g_lockView);
+ printf("prove-ownership wipe transition: closeToLock ret=%d lock=%d forget=%d wipe=%d\r\n",
+ closeRet, g_lockView.isActive, g_forgetPassView.isActive, g_wipeDeviceView.isActive);
+ } else {
+ printf("prove-ownership wipe transition: lock view not opened, skip closeToLock\r\n");
+ }
+ GuiWipeDeviceSetForced(true);
+ int32_t openRet = GuiFrameOpenView(&g_wipeDeviceView);
+ printf("prove-ownership wipe transition: openWipe ret=%d lock=%d forget=%d wipe=%d\r\n",
+ openRet, g_lockView.isActive, g_forgetPassView.isActive, g_wipeDeviceView.isActive);
+ } else {
+ GuiShowPasswordErrorHintBox(keyboardWidget);
+ }
}
}
@@ -450,6 +503,11 @@ static void GuiShowPasswordErrorHintBox(KeyboardWidget_t *keyboardWidget)
lv_obj_t *errHintBox = GuiCreateResultHintbox(386, &imgFailed,
_("unlock_device_error_attempts_exceed"), _("unlock_device_error_attempts_exceed_desc"),
NULL, DARK_GRAY_COLOR, _("unlock_device_error_btn_start_text"), DARK_GRAY_COLOR);
+ if (keyboardWidget->keyboardHintBox != NULL &&
+ lv_obj_get_parent(keyboardWidget->keyboardHintBox) == lv_layer_top()) {
+ lv_obj_set_parent(errHintBox, lv_layer_top());
+ lv_obj_move_foreground(errHintBox);
+ }
lv_obj_t *btn = GuiGetHintBoxRightBtn(errHintBox);
lv_label_set_text(lv_obj_get_child(btn, 0), _("unlock_device_error_btn_start_text"));
@@ -464,8 +522,8 @@ static void GuiShowPasswordErrorHintBox(KeyboardWidget_t *keyboardWidget)
static void LockDeviceHandler(lv_event_t *e)
{
KeyboardWidget_t *keyboardWidget = (KeyboardWidget_t *)lv_event_get_user_data(e);
- GuiHintBoxToLockSreen();
GuiDeleteKeyboardWidget(keyboardWidget);
+ GuiHintBoxToLockSreen();
}
static void GuiHintBoxToLockSreen(void)
@@ -478,6 +536,7 @@ static void GuiHintBoxToLockSreen(void)
}
}
+
static void CountDownHandler(lv_timer_t *timer)
{
KeyboardWidget_t *keyboardWidget = (KeyboardWidget_t *)timer->user_data;
@@ -491,8 +550,8 @@ static void CountDownHandler(lv_timer_t *timer)
}
if (*keyboardWidget->timerCounter <= 0) {
- GuiHintBoxToLockSreen();
GuiDeleteKeyboardWidget(keyboardWidget);
+ GuiHintBoxToLockSreen();
}
}
diff --git a/src/ui/gui_components/gui_keyboard_hintbox.h b/src/ui/gui_components/gui_keyboard_hintbox.h
index 007d721..d974dc2 100644
--- a/src/ui/gui_components/gui_keyboard_hintbox.h
+++ b/src/ui/gui_components/gui_keyboard_hintbox.h
@@ -16,6 +16,8 @@ typedef struct KeyboardWidget {
lv_obj_t *led[6];
lv_obj_t *btnm;
lv_obj_t *errLabel;
+ lv_obj_t *titleLabel;
+ lv_obj_t *noticeLabel;
lv_obj_t *eyeImg;
lv_obj_t *switchLabel;
uint8_t currentNum;
@@ -30,6 +32,7 @@ typedef struct KeyboardWidget {
KeyboardWidget_t *GuiCreateKeyboardWidget(lv_obj_t *parent);
KeyboardWidget_t *GuiCreateKeyboardWidgetView(lv_obj_t *parent, lv_event_cb_t buttonCb, uint16_t *signal);
void SetKeyboardWidgetSig(KeyboardWidget_t *keyboardWidget, uint16_t *sig);
+void SetKeyboardWidgetTitle(KeyboardWidget_t *keyboardWidget, const char *title, const char *desc);
void SetKeyboardWidgetSelf(KeyboardWidget_t *keyboardWidget, KeyboardWidget_t **self);
void SetKeyboardWidgetMode(uint8_t mode);
uint8_t GetKeyboardWidgetMode(void);
diff --git a/src/ui/gui_model/gui_model.c b/src/ui/gui_model/gui_model.c
index 9caad95..c306fae 100644
--- a/src/ui/gui_model/gui_model.c
+++ b/src/ui/gui_model/gui_model.c
@@ -25,6 +25,7 @@
#include "screen_manager.h"
#include "keystore.h"
#include "account_manager.h"
+#include "se_manager.h"
#include "qrdecode_task.h"
#include "gui_views.h"
#include "assert.h"
@@ -114,6 +115,11 @@ static int32_t ModelUpdateBoot(const void *inData, uint32_t inDataLen);
static PasswordVerifyResult_t g_passwordVerifyResult;
static bool g_stopCalChecksum = false;
+// Forget-pass: the account index the entered mnemonic matches (the wallet being reset), captured at the
+// mnemonic-verify step (ModelBip39/Slip39ForgetPass). Read at prove-ownership to refuse a proof made with THAT
+// wallet's own password — ownership must be proven via a DIFFERENT wallet. 0xFF = unset (re-set on every
+// mnemonic verify before prove).
+static uint8_t g_forgetResetIndex = 0xFF;
#ifdef COMPILE_SIMULATOR
// On the real device, AsyncExecute posts to a FreeRTOS background task
@@ -679,7 +685,10 @@ static int32_t ModelBip39ForgetPass(const void *inData, uint32_t inDataLen)
do {
ret = CHECK_BATTERY_LOW_POWER();
CHECK_ERRCODE_BREAK("save low power", ret);
- ret = ModelComparePubkey(MNEMONIC_TYPE_BIP39, NULL, 0, 0, false, 0, NULL);
+ // Capture the matched (reset) wallet index for the prove-ownership guard. ModelComparePubkey returns
+ // ERR_KEYSTORE_MNEMONIC_REPEAT (!= SUCCESS) exactly when the mnemonic matches an existing wallet — the
+ // forget-pass SUCCESS path — and sets *index to that wallet.
+ ret = ModelComparePubkey(MNEMONIC_TYPE_BIP39, NULL, 0, 0, false, 0, &g_forgetResetIndex);
if (ret != SUCCESS_CODE) {
GuiApiEmitSignal(SIG_FORGET_PASSWORD_SUCCESS, NULL, 0);
SetLockScreen(enable);
@@ -1045,7 +1054,8 @@ static int32_t ModelSlip39ForgetPass(const void *inData, uint32_t inDataLen)
printf("get master secret error\n");
break;
}
- ret = ModelComparePubkey(MNEMONIC_TYPE_SLIP39, ems, entropyLen, id, eb, ie, NULL);
+ // Capture the matched (reset) wallet index for the prove-ownership guard (see ModelBip39ForgetPass).
+ ret = ModelComparePubkey(MNEMONIC_TYPE_SLIP39, ems, entropyLen, id, eb, ie, &g_forgetResetIndex);
if (ret != SUCCESS_CODE) {
GuiApiEmitSignal(SIG_FORGET_PASSWORD_SUCCESS, NULL, 0);
SetLockScreen(enable);
@@ -1086,7 +1096,7 @@ static int32_t ModelDelWallet(const void *inData, uint32_t inDataLen)
uint8_t accountIndex = GetCurrentAccountIndex();
UpdateFingerSignFlag(accountIndex, false);
CloseUsb();
- ret = DestroyAccount(accountIndex);
+ ret = DestroyAccount(accountIndex); // gen-2 SE key rotation now happens inside DestroyAccount
if (ret == SUCCESS_CODE) {
// reset address index in receive page
{
@@ -1166,10 +1176,22 @@ static int32_t ModelChangeAccountPass(const void *inData, uint32_t inDataLen)
#ifndef COMPILE_SIMULATOR
int32_t ret;
+ // Gate change-PIN on low battery like wallet creation (MODEL_WRITE_SE_HEAD) so it can't start on a dying
+ // battery and be interrupted mid-flight.
+ ret = CHECK_BATTERY_LOW_POWER();
+ if (ret != SUCCESS_CODE) {
+ GuiApiEmitSignal(SIG_SETTING_CHANGE_PASSWORD_FAIL, NULL, 0);
+ ClearSecretCache();
+ SetLockScreen(enable);
+ return SUCCESS_CODE;
+ }
+
ret = VerifyCurrentAccountPassword(SecretCacheGetPassword());
- ret = ChangePassword(GetCurrentAccountIndex(), SecretCacheGetNewPassword(), SecretCacheGetPassword());
- UpdateFingerSignFlag(GetCurrentAccountIndex(), false);
if (ret == SUCCESS_CODE) {
+ ret = ChangePassword(GetCurrentAccountIndex(), SecretCacheGetNewPassword(), SecretCacheGetPassword());
+ }
+ if (ret == SUCCESS_CODE) {
+ UpdateFingerSignFlag(GetCurrentAccountIndex(), false);
GuiApiEmitSignal(SIG_SETTING_CHANGE_PASSWORD_PASS, NULL, 0);
} else {
GuiApiEmitSignal(SIG_SETTING_CHANGE_PASSWORD_FAIL, NULL, 0);
@@ -1281,6 +1303,10 @@ static void ModelVerifyPassSuccess(uint16_t *param)
case SIG_SETUP_RSA_PRIVATE_KEY_WITH_PASSWORD:
GuiApiEmitSignal(SIG_SETUP_RSA_PRIVATE_KEY_RSA_VERIFY_PASSWORD_PASS, param, sizeof(*param));
break;
+ case SIG_FORGET_PASSWORD_PROVE_OWNERSHIP:
+ // advance the forget-pass flow to set the new PIN.
+ GuiApiEmitSignal(SIG_FORGET_PASSWORD_PROVE_OWNERSHIP_PASS, param, sizeof(*param));
+ break;
default:
GuiApiEmitSignal(SIG_VERIFY_PASSWORD_PASS, param, sizeof(*param));
break;
@@ -1293,6 +1319,7 @@ static void ModelVerifyPassFailed(uint16_t *param)
switch (*param) {
case SIG_LOCK_VIEW_VERIFY_PIN:
case SIG_LOCK_VIEW_SCREEN_GO_HOME_PASS:
+ case SIG_FORGET_PASSWORD_PROVE_OWNERSHIP: // prove-ownership uses the login error counter
g_passwordVerifyResult.errorCount = GetLoginPasswordErrorCount();
printf("gui model get login error count %d \n", g_passwordVerifyResult.errorCount);
assert(g_passwordVerifyResult.errorCount <= MAX_LOGIN_PASSWORD_ERROR_COUNT);
@@ -1349,10 +1376,37 @@ static int32_t ModelVerifyAccountPass(const void *inData, uint32_t inDataLen)
} else if (ret == SUCCESS_CODE) {
ModeGetWalletDesc(NULL, 0);
}
+ } else if (SIG_FORGET_PASSWORD_PROVE_OWNERSHIP == *param) {
+ // Forget-pass (gen-2 multi-wallet): prove device ownership by authenticating ANY existing wallet.
+ // Try-all check (no login), ticking the login error counter, clamped at MAX_LOGIN so it stops at the
+ // wipe threshold instead of running past it.
+ ret = VerifyOwnershipPasswordTryAll(&accountIndex, SecretCacheGetPassword(),
+ MAX_LOGIN_PASSWORD_ERROR_COUNT);
+ // Ownership must be proven via a DIFFERENT wallet than the one being reset. If the entered password
+ // matches the reset wallet ITSELF (g_forgetResetIndex, resolved at the mnemonic-verify step), reject the
+ // proof. Unreachable in normal use (unique passwords mean only its own password maps to its index) —
+ // deliberate belt-and-suspenders.
+ if (ret == SUCCESS_CODE && accountIndex == g_forgetResetIndex) {
+ ret = ERR_KEYSTORE_PASSWORD_ERR;
+ }
} else {
ret = VerifyCurrentAccountPassword(SecretCacheGetPassword());
}
+ // gen-2 forget-pass ONLY: ownership proven via another wallet -> arm provision-recovery on THAT matched
+ // wallet for the upcoming re-provision (CreateNewAccount of the forgotten wallet).
+ if (ret == SUCCESS_CODE && *param == SIG_FORGET_PASSWORD_PROVE_OWNERSHIP && GetSeGen() == SE_GEN_2) {
+ SE_ArmProvisionRecovery(accountIndex, SecretCacheGetPassword());
+ }
+
+ // gen-2 add-wallet ONLY: the existing wallet is authenticated here. Capture its index + passcode into the
+ // dedicated provision-recovery holder while both are valid (the secret cache is wiped right after this by
+ // the add-wallet navigation), for the upcoming SetNewKeyPieceToSE. gen-1 has nothing to recover and no
+ // reason to retain the passcode.
+ if (ret == SUCCESS_CODE && *param == DEVICE_SETTING_ADD_WALLET && GetSeGen() == SE_GEN_2) {
+ SE_ArmProvisionRecovery(GetCurrentAccountIndex(), SecretCacheGetPassword());
+ }
+
if (SIG_LOCK_VIEW_VERIFY_PIN == *param && firstVerify && ModelGetPassphraseQuickAccess()) {
*param = SIG_LOCK_VIEW_SCREEN_ON_VERIFY_PASSPHRASE;
firstVerify = false;
@@ -1370,6 +1424,11 @@ static int32_t ModelVerifyAccountPass(const void *inData, uint32_t inDataLen)
*param != SIG_MULTISIG_WALLET_DELETE_VERIFY_PASSWORD &&
*param != SIG_HARDWARE_CALL_DERIVE_PUBKEY &&
*param != SIG_INIT_CONNECT_USB &&
+ // forget-pass prove-ownership: the user already entered the seed; clearing it here would blank the
+ // mnemonic before the upcoming re-save (it would fail with "mnemonic error"). The other wallet's PIN
+ // we just verified is already captured in the dedicated provision-recovery holder; the forget-pass
+ // DeInit clears the cache.
+ *param != SIG_FORGET_PASSWORD_PROVE_OWNERSHIP &&
!strnlen_s(SecretCacheGetPassphrase(), PASSPHRASE_MAX_LEN) &&
!GuiCheckIfViewOpened(&g_createWalletView) &&
!ModelGetPassphraseQuickAccess()) {
diff --git a/src/ui/gui_model/gui_model.h b/src/ui/gui_model/gui_model.h
index a0682f0..a0459b4 100644
--- a/src/ui/gui_model/gui_model.h
+++ b/src/ui/gui_model/gui_model.h
@@ -102,4 +102,3 @@ void GuiModelTransactionParseRawDataDelay(void);
#endif /* _GUI_MODEL_H */
-
diff --git a/src/ui/gui_views/gui_forget_pass_view.c b/src/ui/gui_views/gui_forget_pass_view.c
index 34c7c76..8aaf8e2 100644
--- a/src/ui/gui_views/gui_forget_pass_view.c
+++ b/src/ui/gui_views/gui_forget_pass_view.c
@@ -55,10 +55,17 @@ int32_t GuiForgetViewEventProcess(void *self, uint16_t usEvent, void *param, uin
} else {
return ERR_GUI_ERROR;
}
+ if (tileIndex == SIG_FORGET_PASSWORD_PROVE_OWNERSHIP) {
+ GuiForgetProveOwnershipResult(false, param); // show attempts on the prove-ownership tile
+ break;
+ }
// GuiForgetPassCode(false, tileIndex);
GuiLockScreenPassCode(false);
GuiLockScreenErrorCount(param);
break;
+ case SIG_FORGET_PASSWORD_PROVE_OWNERSHIP_PASS:
+ GuiForgetProveOwnershipResult(true, NULL); // ownership proven -> advance to set new PIN
+ break;
case SIG_SETTING_SET_PIN:
GuiForgetPassSetPinPass((const char *)param);
break;
diff --git a/src/ui/gui_views/gui_views.h b/src/ui/gui_views/gui_views.h
index 39fa976..98e233c 100644
--- a/src/ui/gui_views/gui_views.h
+++ b/src/ui/gui_views/gui_views.h
@@ -137,6 +137,8 @@ typedef enum {
SIG_FORGET_PASSWORD_SUCCESS = SIG_FINGER_SET_BUTT + 50,
SIG_FORGET_PASSWORD_FAIL,
+ SIG_FORGET_PASSWORD_PROVE_OWNERSHIP, // passcode-verify param: prove device ownership (gen-2 multi-wallet forget-pass)
+ SIG_FORGET_PASSWORD_PROVE_OWNERSHIP_PASS, // result: ownership proven -> advance to set new PIN
SIG_FORGET_PASSWORD_BUTT,
SIG_WEB_AUTH_CODE_SUCCESS = SIG_FORGET_PASSWORD_BUTT + 50,
diff --git a/src/ui/gui_widgets/gui_about_info_widgets.c b/src/ui/gui_widgets/gui_about_info_widgets.c
index 752a351..dc402fa 100644
--- a/src/ui/gui_widgets/gui_about_info_widgets.c
+++ b/src/ui/gui_widgets/gui_about_info_widgets.c
@@ -14,6 +14,7 @@
#include "err_code.h"
#include "secret_cache.h"
#include "fingerprint_process.h"
+#include "se_manager.h"
#include "log.h"
#ifndef COMPILE_SIMULATOR
#include "drv_battery.h"
@@ -146,6 +147,9 @@ void GuiAboutInfoEntranceWidget(lv_obj_t *parent)
titleLabel = GuiCreateTextLabel(parent, _("about_info_firmware_version"));
contentLabel = GuiCreateNoticeLabel(parent, versionStr);
GuiGetFpVersion(&fpVersion[1], sizeof(fpVersion) - 1);
+ // Append the SE generation so the row shows FP firmware + SE version together (e.g. "v1.0.0.0&V2.0").
+ strncat(fpVersion, (GetSeGen() == SE_GEN_2) ? "&v2.0" : "&v1.0",
+ sizeof(fpVersion) - strlen(fpVersion) - 1);
GuiButton_t table[] = {
{.obj = titleLabel, .align = LV_ALIGN_DEFAULT, .position = {24, 24}},
diff --git a/src/ui/gui_widgets/gui_create_wallet_widgets.c b/src/ui/gui_widgets/gui_create_wallet_widgets.c
index b7e1c19..5ac051c 100644
--- a/src/ui/gui_widgets/gui_create_wallet_widgets.c
+++ b/src/ui/gui_widgets/gui_create_wallet_widgets.c
@@ -6,6 +6,7 @@
#include "gui_hintbox.h"
#include "gui_create_wallet_widgets.h"
#include "gui_model.h"
+#include "se_manager.h"
#include "secret_cache.h"
#include "user_memory.h"
#include "gui_enter_passcode.h"
@@ -326,6 +327,7 @@ static void GuiImportBackupWidget(lv_obj_t *parent, bool enablePassphrase)
void GuiCreateWalletInit(uint8_t walletMethod)
{
+ GuiResetCheckPasswordCounter(); // reset the per-flow password-check try counter
CLEAR_OBJECT(g_createWalletTileView);
g_selectedEntropyMethod = ENTROPY_TYPE_STANDARD;
@@ -359,6 +361,12 @@ void GuiCreateWalletInit(uint8_t walletMethod)
lv_obj_set_tile_id(g_createWalletTileView.tileView, g_createWalletTileView.currentTile, 0, LV_ANIM_OFF);
lv_obj_clear_flag(tileView, LV_OBJ_FLAG_SCROLLABLE);
+
+ // Hold the auto-lock off for the whole create/add-wallet flow: the user pauses here for minutes to write
+ // down the mnemonic, and an inactivity auto-lock mid-flow tears down this view (DeInit) and logs the user
+ // out, which aborts the in-progress add-wallet operation. Re-enabled in GuiCreateWalletDeInit so it can
+ // never stay stuck off.
+ SetPageLockScreen(false);
}
int8_t GuiCreateWalletNextTile(void)
@@ -464,6 +472,22 @@ void GuiCreateWalletDeInit(void)
g_createWalletTileView.currentTile = 0;
CLEAR_OBJECT(g_createWalletTileView);
ClearSecretCache();
+ // Re-enable the auto-lock disabled in GuiCreateWalletInit — but ONLY when no add-wallet provision is
+ // pending. In add-wallet this view is a re-openable sub-step under the notice tile, which OWNS the page-lock
+ // hold for the whole operation. SetPageLockScreen is a plain bool (not refcounted), so blindly setting
+ // it true here would clobber that hold: a subsequent auto-lock / power-button lock would then pass the
+ // LockScreen guards and disarm the pending operation. When nothing is pending (first-wallet setup, or the
+ // op already ended/was consumed) re-enabling is correct; the notice-tile destruct
+ // (GuiSettingCountDownDestruct) restores auto-lock at the true add-wallet boundary.
+ if (!SE_IsProvisionRecoveryArmed()) {
+ SetPageLockScreen(true);
+ }
+ // Do NOT SE_DisarmProvisionRecovery() here. This view is a re-openable sub-step of add-wallet: backing out
+ // of the set-pin tile (CloseCurrentViewHandler) tears this view down but leaves the user inside add-wallet
+ // at the notice tile, and re-entering set-pin never re-runs the verify that armed the provision. Disarming
+ // here would drop the arm mid-flow, so the eventual provision fails. The arm is dropped at the true
+ // add-wallet boundary instead: GuiSettingCountDownDestruct (notice-tile teardown) on cancel, and
+ // SetNewKeyPieceToSE consumes it on a successful provision.
if (g_pageWidget != NULL) {
DestroyPageWidget(g_pageWidget);
g_pageWidget = NULL;
diff --git a/src/ui/gui_widgets/gui_enter_passcode.c b/src/ui/gui_widgets/gui_enter_passcode.c
index 67f3dd9..6b004d7 100644
--- a/src/ui/gui_widgets/gui_enter_passcode.c
+++ b/src/ui/gui_widgets/gui_enter_passcode.c
@@ -2,6 +2,7 @@
#include "gui_obj.h"
#include "gui_led.h"
#include "gui_views.h"
+#include "gui_framework.h"
#include "gui_button.h"
#include "user_memory.h"
#include "secret_cache.h"
@@ -12,8 +13,10 @@
#include "motor_manager.h"
#include "account_manager.h"
#include "gui_keyboard_hintbox.h"
+#include "gui_hintbox.h"
#include "drv_mpu.h"
#include "device_setting.h"
+#include "screen_manager.h"
typedef enum {
PASSWORD_STRENGTH_LEN,
@@ -36,6 +39,7 @@ static EnterPassCodeParam_t g_passParam;
static bool g_isHandle = true;
#define SET_HANDLE_FLAG() (g_isHandle = true)
#define CLEAR_HANDLE_FLAG() (g_isHandle = false)
+#define MAX_CHECK_PASSWORD_COUNTER 10
typedef struct EnterPassLabel {
const char *title;
@@ -43,6 +47,271 @@ typedef struct EnterPassLabel {
const char *passSwitch;
} EnterPassLabel_t;
static EnterPassLabel_t g_enterPassLabel[ENTER_PASSCODE_BUTT];
+static lv_obj_t *g_weakPasscodeHintBox = NULL;
+static GuiEnterPasscodeItem_t *g_weakPasscodeItem = NULL;
+static char g_weakPasscodeBuf[PASSWORD_MAX_LEN + 1] = {0};
+static uint8_t g_checkPasswordCounter = 0;
+
+static uint8_t GetPasswordCheckExcludeIndex(void)
+{
+ void *userParam = g_passParam.userParam;
+ if (userParam != NULL && *(uint16_t *)userParam == DEVICE_SETTING_RESET_PASSCODE_VERIFY) {
+ return GetCurrentAccountIndex();
+ }
+ return 0xff;
+}
+
+// Reset the per-flow duplicate-check counter. Called once at each set-new-passcode flow ENTRY
+// (create/add wallet, change password, forget-pass reset) and when a flow is dropped. Must NOT
+// be called from GuiCreateEnterPasscode: the SET_PIN widget is rebuilt on every retry within a flow, which
+// would reset the counter on each input.
+void GuiResetCheckPasswordCounter(void)
+{
+ if (g_checkPasswordCounter != 0) {
+ printf("reset check password counter=%d\r\n", g_checkPasswordCounter);
+ }
+ g_checkPasswordCounter = 0;
+}
+
+static int32_t CheckPasswordExistedWithCounter(const char *password, uint8_t excludeIndex, bool *limitReached)
+{
+ int32_t ret;
+
+ *limitReached = false;
+ if (g_checkPasswordCounter < MAX_CHECK_PASSWORD_COUNTER) {
+ g_checkPasswordCounter++;
+ }
+ printf("check password counter=%d\r\n", g_checkPasswordCounter);
+ ret = CheckPasswordExisted(password, excludeIndex);
+ if (g_checkPasswordCounter >= MAX_CHECK_PASSWORD_COUNTER) {
+ *limitReached = true;
+ }
+ return ret;
+}
+
+static bool RecordDupPasswordIntentLimitReached(void)
+{
+ return RecordCurrentPasswordError(MAX_CURRENT_PASSWORD_ERROR_COUNT_SHOW_HINTBOX) >=
+ MAX_CURRENT_PASSWORD_ERROR_COUNT_SHOW_HINTBOX;
+}
+
+static void GuiAbortSetPasscodeFlow(void)
+{
+ if (g_homeView.isActive) {
+ GuiCloseToTargetView(&g_homeView);
+ return;
+ }
+
+ if (g_createWalletView.isActive) {
+ GuiFrameCLoseView(&g_createWalletView);
+ }
+ if (g_settingView.isActive) {
+ GuiFrameCLoseView(&g_settingView);
+ }
+ if (g_forgetPassView.isActive) {
+ GuiFrameCLoseView(&g_forgetPassView);
+ }
+}
+
+static void DropSetPasscodeFlowAsyncCb(void *userData)
+{
+ (void)userData;
+ // The duplicate-check limit for this flow is reached. Drop the set-passcode flow (create/add
+ // wallet, change password, forget-pass reset) back to its parent and reset the counter — no lock view /
+ // re-verify is needed. Avoiding the lock view also avoids the cross-task LVGL / deep-sleep hazards that
+ // came with forcing a lock from here.
+ GuiResetCheckPasswordCounter();
+ GuiAbortSetPasscodeFlow();
+}
+
+static void GuiDropSetPasscodeFlow(void)
+{
+ // Deferred out of the current LVGL event callback. Every caller runs inside a keypad/keyboard/modal event
+ // handler (LV_EVENT_RELEASED / READY / CLICKED), and DropSetPasscodeFlowAsyncCb() tears down the
+ // create-wallet/setting view stack — including the button matrix whose handler is on the stack. Doing that
+ // synchronously frees the widget mid-dispatch; LVGL keeps touching it after the handler returns and corrupts
+ // the heap. Run it on the next lv_timer_handler tick, once the event chain has fully unwound.
+ lv_async_call(DropSetPasscodeFlowAsyncCb, NULL);
+}
+
+static void GuiClearSetPinInput(GuiEnterPasscodeItem_t *item)
+{
+ if (item == NULL) {
+ return;
+ }
+ for (int i = 0; i < CREATE_PIN_NUM; i++) {
+ GuiSetLedStatus(item->numLed[i], PASSCODE_LED_OFF);
+ }
+ item->currentNum = 0;
+ memset_s(g_pinBuf, sizeof(g_pinBuf), 0, sizeof(g_pinBuf));
+}
+
+static void GuiClearSetPasswordInput(GuiEnterPasscodeItem_t *item)
+{
+ if (item == NULL || item->kb == NULL || item->kb->ta == NULL) {
+ return;
+ }
+ lv_textarea_set_text(item->kb->ta, "");
+ if (item->scoreBar != NULL && !lv_obj_has_flag(item->scoreBar, LV_OBJ_FLAG_HIDDEN)) {
+ lv_obj_add_flag(item->scoreBar, LV_OBJ_FLAG_HIDDEN);
+ }
+ if (item->scoreLevel != NULL && !lv_obj_has_flag(item->scoreLevel, LV_OBJ_FLAG_HIDDEN)) {
+ lv_obj_add_flag(item->scoreLevel, LV_OBJ_FLAG_HIDDEN);
+ }
+ if (item->lenOverLabel != NULL && !lv_obj_has_flag(item->lenOverLabel, LV_OBJ_FLAG_HIDDEN)) {
+ lv_obj_add_flag(item->lenOverLabel, LV_OBJ_FLAG_HIDDEN);
+ }
+}
+
+static void GuiClearWeakPasscodeInput(GuiEnterPasscodeItem_t *item)
+{
+ if (item == NULL) {
+ return;
+ }
+ if (item->mode == ENTER_PASSCODE_SET_PIN) {
+ GuiClearSetPinInput(item);
+ } else if (item->mode == ENTER_PASSCODE_SET_PASSWORD) {
+ GuiClearSetPasswordInput(item);
+ }
+ if (item->errLabel != NULL && !lv_obj_has_flag(item->errLabel, LV_OBJ_FLAG_HIDDEN)) {
+ lv_obj_add_flag(item->errLabel, LV_OBJ_FLAG_HIDDEN);
+ }
+ if (item->repeatLabel != NULL && !lv_obj_has_flag(item->repeatLabel, LV_OBJ_FLAG_HIDDEN)) {
+ lv_obj_add_flag(item->repeatLabel, LV_OBJ_FLAG_HIDDEN);
+ }
+}
+
+static bool IsSameDigitPin(const char *pin)
+{
+ for (int i = 1; i < CREATE_PIN_NUM; i++) {
+ if (pin[i] != pin[0]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool IsSequentialPin(const char *pin, int8_t step)
+{
+ for (int i = 1; i < CREATE_PIN_NUM; i++) {
+ if ((pin[i] - pin[i - 1]) != step) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool IsWeakPin(const char *pin)
+{
+ if (pin == NULL || strnlen_s(pin, PASSWORD_MAX_LEN) != CREATE_PIN_NUM) {
+ return false;
+ }
+
+ const char *commonPins[] = {
+ "000000", "111111", "112233", "123123", "123456", "654321"
+ };
+ for (uint8_t i = 0; i < sizeof(commonPins) / sizeof(commonPins[0]); i++) {
+ if (strcmp(pin, commonPins[i]) == 0) {
+ return true;
+ }
+ }
+
+ if (IsSameDigitPin(pin) || IsSequentialPin(pin, 1) || IsSequentialPin(pin, -1)) {
+ return true;
+ }
+
+ if (pin[0] == pin[1] && pin[1] == pin[2] && pin[3] == pin[4] && pin[4] == pin[5]) {
+ return true;
+ }
+
+ if (pin[0] == pin[3] && pin[1] == pin[4] && pin[2] == pin[5]) {
+ return true;
+ }
+
+ if (pin[0] == pin[2] && pin[2] == pin[4] && pin[1] == pin[3] && pin[3] == pin[5]) {
+ return true;
+ }
+
+ if (pin[0] == pin[1] && pin[2] == pin[3] && pin[4] == pin[5]) {
+ return true;
+ }
+
+ return false;
+}
+
+static bool IsWeakPassword(const char *password)
+{
+ uint8_t passwordLen = strnlen_s(password, PASSWORD_MAX_LEN);
+ return GetPassWordStrength(password, passwordLen) <= 40;
+}
+
+static void WeakPasscodeModalClose(void)
+{
+ if (g_weakPasscodeHintBox != NULL && lv_obj_is_valid(g_weakPasscodeHintBox)) {
+ lv_obj_del(g_weakPasscodeHintBox);
+ }
+ g_weakPasscodeHintBox = NULL;
+}
+
+static void WeakPasscodeContinueHandler(lv_event_t *e)
+{
+ char passcode[PASSWORD_MAX_LEN + 1] = {0};
+ strcpy_s(passcode, sizeof(passcode), g_weakPasscodeBuf);
+ GuiEnterPasscodeItem_t *item = g_weakPasscodeItem;
+
+ WeakPasscodeModalClose();
+ g_weakPasscodeItem = NULL;
+ memset_s(g_weakPasscodeBuf, sizeof(g_weakPasscodeBuf), 0, sizeof(g_weakPasscodeBuf));
+
+ bool limitReached = false;
+ int32_t ret = CheckPasswordExistedWithCounter(passcode, GetPasswordCheckExcludeIndex(), &limitReached);
+ if (limitReached) {
+ GuiClearWeakPasscodeInput(item);
+ UnlimitedVibrate(SUPER_LONG);
+ GuiDropSetPasscodeFlow();
+ return;
+ }
+ if (ret != SUCCESS_CODE) {
+ GuiClearWeakPasscodeInput(item);
+ if (ret == ERR_KEYSTORE_REPEAT_PASSWORD && RecordDupPasswordIntentLimitReached()) {
+ UnlimitedVibrate(SUPER_LONG);
+ GuiDropSetPasscodeFlow();
+ } else {
+ UnlimitedVibrate(LONG);
+ if (item != NULL && item->repeatLabel != NULL) {
+ lv_obj_clear_flag(item->repeatLabel, LV_OBJ_FLAG_HIDDEN);
+ }
+ }
+ return;
+ }
+ GuiClearWeakPasscodeInput(item);
+ GuiEmitSignal(SIG_SETTING_SET_PIN, passcode, strnlen_s(passcode, PASSWORD_MAX_LEN));
+}
+
+static void WeakPasscodeChangeHandler(lv_event_t *e)
+{
+ WeakPasscodeModalClose();
+ GuiClearWeakPasscodeInput(g_weakPasscodeItem);
+ g_weakPasscodeItem = NULL;
+ memset_s(g_weakPasscodeBuf, sizeof(g_weakPasscodeBuf), 0, sizeof(g_weakPasscodeBuf));
+}
+
+static void GuiShowWeakPasscodeHintBox(GuiEnterPasscodeItem_t *item, const char *passcode)
+{
+ WeakPasscodeModalClose();
+ g_weakPasscodeItem = item;
+ memset_s(g_weakPasscodeBuf, sizeof(g_weakPasscodeBuf), 0, sizeof(g_weakPasscodeBuf));
+ strcpy_s(g_weakPasscodeBuf, sizeof(g_weakPasscodeBuf), passcode);
+
+ g_weakPasscodeHintBox = GuiCreateGeneralHintBox(&imgWarn, _("weak_passcode_warning_title"),
+ _("weak_passcode_warning_desc"), NULL,
+ _("Continue"), DARK_GRAY_COLOR,
+ _("weak_passcode_warning_change"), DEEP_ORANGE_COLOR);
+ lv_obj_t *leftBtn = GuiGetHintBoxLeftBtn(g_weakPasscodeHintBox);
+ lv_obj_add_event_cb(leftBtn, WeakPasscodeContinueHandler, LV_EVENT_CLICKED, NULL);
+ lv_obj_t *rightBtn = GuiGetHintBoxRightBtn(g_weakPasscodeHintBox);
+ lv_obj_add_event_cb(rightBtn, WeakPasscodeChangeHandler, LV_EVENT_CLICKED, NULL);
+}
void GuiEnterPassLabelRefresh(void)
{
@@ -116,39 +385,56 @@ static void SetPinEventHandler(lv_event_t *e)
}
if (item->currentNum == CREATE_PIN_NUM) {
- for (int i = 0; i < CREATE_PIN_NUM; i++) {
- GuiSetLedStatus(item->numLed[i], PASSCODE_LED_OFF);
- }
-
g_userParam = g_passParam.userParam;
- uint8_t index = 0xff;
- if (g_userParam != NULL && *(uint16_t *)g_userParam == DEVICE_SETTING_RESET_PASSCODE_VERIFY) {
- index = GetCurrentAccountIndex();
+ if (item->mode == ENTER_PASSCODE_SET_PIN) {
+ // Check weakness first; it is local and avoids an unnecessary duplicate check.
+ if (IsWeakPin(g_pinBuf)) {
+ GuiShowWeakPasscodeHintBox(item, g_pinBuf);
+ return;
+ }
+ bool limitReached = false;
+ int32_t ret = CheckPasswordExistedWithCounter(g_pinBuf, GetPasswordCheckExcludeIndex(), &limitReached);
+ if (limitReached) {
+ GuiClearSetPinInput(item);
+ UnlimitedVibrate(SUPER_LONG);
+ GuiDropSetPasscodeFlow();
+ return;
+ }
+ if (ret != SUCCESS_CODE) {
+ GuiClearSetPinInput(item);
+ if (ret == ERR_KEYSTORE_REPEAT_PASSWORD && RecordDupPasswordIntentLimitReached()) {
+ UnlimitedVibrate(SUPER_LONG);
+ GuiDropSetPasscodeFlow();
+ return;
+ } else {
+ UnlimitedVibrate(LONG);
+ lv_obj_clear_flag(item->repeatLabel, LV_OBJ_FLAG_HIDDEN);
+ }
+ item->setPassCb = NULL;
+ return;
+ }
}
+ char completedPin[PASSWORD_MAX_LEN + 1] = {0};
+ strcpy_s(completedPin, sizeof(completedPin), g_pinBuf);
+ GuiClearSetPinInput(item);
+
switch (item->mode) {
case ENTER_PASSCODE_VERIFY_PIN:
- SecretCacheSetPassword(g_pinBuf);
+ SecretCacheSetPassword(completedPin);
GuiLockScreenShowVerifyLoading(g_userParam);
GuiModelVerifyAccountPassWord(g_userParam);
break;
case ENTER_PASSCODE_SET_PIN:
- if (CheckPasswordExisted(g_pinBuf, index)) {
- UnlimitedVibrate(LONG);
- lv_obj_clear_flag(item->repeatLabel, LV_OBJ_FLAG_HIDDEN);
- } else {
- GuiEmitSignal(SIG_SETTING_SET_PIN, g_pinBuf, strnlen_s(g_pinBuf, PASSWORD_MAX_LEN));
- }
+ GuiEmitSignal(SIG_SETTING_SET_PIN, completedPin, strnlen_s(completedPin, PASSWORD_MAX_LEN));
break;
case ENTER_PASSCODE_REPEAT_PIN:
- GuiEmitSignal(SIG_SETTING_REPEAT_PIN, g_pinBuf, strnlen_s(g_pinBuf, PASSWORD_MAX_LEN));
+ GuiEmitSignal(SIG_SETTING_REPEAT_PIN, completedPin, strnlen_s(completedPin, PASSWORD_MAX_LEN));
break;
default:
break;
}
CLEAR_HANDLE_FLAG();
- item->currentNum = 0;
- memset_s(g_pinBuf, sizeof(g_pinBuf), 0, sizeof(g_pinBuf));
item->setPassCb = NULL;
}
}
@@ -183,15 +469,30 @@ static void SetPassWordHandler(lv_event_t *e)
} else {
g_userParam = g_passParam.userParam;
if (item->mode == ENTER_PASSCODE_SET_PASSWORD) {
- uint8_t index = 0xff;
-
- if (g_userParam != NULL && *(uint16_t *)g_userParam == DEVICE_SETTING_RESET_PASSCODE_VERIFY) {
- index = GetCurrentAccountIndex();
+ if (IsWeakPassword(currText)) {
+ // Check weakness first; it is local and avoids an unnecessary duplicate check.
+ GuiShowWeakPasscodeHintBox(item, currText);
+ return;
+ }
+ bool limitReached = false;
+ int32_t ret = CheckPasswordExistedWithCounter(currText, GetPasswordCheckExcludeIndex(), &limitReached);
+ if (limitReached) {
+ UnlimitedVibrate(SUPER_LONG);
+ lv_textarea_set_text(ta, "");
+ GuiDropSetPasscodeFlow();
+ return;
}
- if (CheckPasswordExisted(currText, index)) {
- UnlimitedVibrate(LONG);
- lv_obj_clear_flag(item->repeatLabel, LV_OBJ_FLAG_HIDDEN);
- delayFlag = true;
+ if (ret != SUCCESS_CODE) {
+ if (ret == ERR_KEYSTORE_REPEAT_PASSWORD && RecordDupPasswordIntentLimitReached()) {
+ UnlimitedVibrate(SUPER_LONG);
+ lv_textarea_set_text(ta, "");
+ GuiDropSetPasscodeFlow();
+ return;
+ } else {
+ UnlimitedVibrate(LONG);
+ lv_obj_clear_flag(item->repeatLabel, LV_OBJ_FLAG_HIDDEN);
+ delayFlag = true;
+ }
} else {
GuiEmitSignal(SIG_SETTING_SET_PIN, (char *)currText, strnlen_s(currText, CREATE_PIN_NUM));
}
@@ -682,10 +983,30 @@ void GuiDelEnterPasscode(void *obj, void *param)
{
GuiEnterPasscodeItem_t *item = obj;
if (item != NULL) {
- // lv_obj_del(item->pinCont);
- // item->pinCont = NULL;
- // lv_obj_del(item->passWdCont);
- // item->pinCont = NULL;
+ // The weak-passcode warning modal is parented to lv_scr_act() (GuiCreateHintBox), so it outlives this
+ // view's teardown, and its Continue/Change handlers dereference g_weakPasscodeItem. Freeing the item
+ // without closing the modal first leaves a dangling pointer -> use-after-free on the next tap. Tie the
+ // modal's lifetime to the item: if it still references the item being freed, close it and clear the
+ // statics (also wipes the plaintext passcode lingering in g_weakPasscodeBuf).
+ if (g_weakPasscodeItem == item) {
+ WeakPasscodeModalClose();
+ g_weakPasscodeItem = NULL;
+ memset_s(g_weakPasscodeBuf, sizeof(g_weakPasscodeBuf), 0, sizeof(g_weakPasscodeBuf));
+ }
+ // Free the LVGL objects this item created (pinCont / passWdCont are its only two top containers on the
+ // parent; the button matrix, LEDs, err/repeat labels all live inside them and cascade-delete). Without
+ // this, an in-place rebuild on a persistent tile (GuiCreateWalletPrevTile: delete item + recreate on the
+ // same g_setPinTile) orphans a full keypad (~12.7 KB) every set->repeat->back bounce, and leaves dangling
+ // input-device/group references to the orphaned button matrix. lv_obj_is_valid keeps the normal close
+ // path safe (there the page/tile may already be gone by the time this runs).
+ if (item->pinCont != NULL && lv_obj_is_valid(item->pinCont)) {
+ lv_obj_del(item->pinCont);
+ }
+ item->pinCont = NULL;
+ if (item->passWdCont != NULL && lv_obj_is_valid(item->passWdCont)) {
+ lv_obj_del(item->passWdCont);
+ }
+ item->passWdCont = NULL;
SRAM_FREE(item);
}
}
diff --git a/src/ui/gui_widgets/gui_enter_passcode.h b/src/ui/gui_widgets/gui_enter_passcode.h
index aa9ebc0..842e779 100644
--- a/src/ui/gui_widgets/gui_enter_passcode.h
+++ b/src/ui/gui_widgets/gui_enter_passcode.h
@@ -46,6 +46,9 @@ void GuiEnterPassCodeStatus(GuiEnterPasscodeItem_t *item, bool en);
void GuiEnterPassLabelRefresh(void);
void SwitchPasswordModeHandler(lv_event_t *e);
void GuiUpdateEnterPasscodeParam(GuiEnterPasscodeItem_t *item, void *param);
+// Reset the per-flow CheckPasswordExisted budget; call at each set-new-passcode flow entry
+// (create/add wallet, change password, forget-pass reset).
+void GuiResetCheckPasswordCounter(void);
uint8_t GetPassWordStrength(const char *password, uint8_t len);
void GuiFingerPrintStatus(GuiEnterPasscodeItem_t *item, bool en, uint8_t errCnt);
void PassWordPinSwitch(GuiEnterPasscodeItem_t *item);
diff --git a/src/ui/gui_widgets/gui_forget_pass_widgets.c b/src/ui/gui_widgets/gui_forget_pass_widgets.c
index bd5e5a5..a3ef153 100644
--- a/src/ui/gui_widgets/gui_forget_pass_widgets.c
+++ b/src/ui/gui_widgets/gui_forget_pass_widgets.c
@@ -20,6 +20,9 @@
#include "gui_page.h"
#include "gui_keyboard_hintbox.h"
#include "gui_lock_device_widgets.h"
+#include "account_manager.h" // GetExistAccountNum
+#include "se_manager.h" // GetSeGen, SE_DisarmProvisionRecovery
+#include "screen_manager.h" // SetPageLockScreen
#ifdef COMPILE_SIMULATOR
#include "simulator_model.h"
#endif
@@ -58,9 +61,21 @@ static ForgetPassWidget_t g_forgetPassTileView;
static lv_obj_t *g_waitAnimCont;
static GUI_VIEW *g_prevView;
static bool g_isForgetPass = false;
+static KeyboardWidget_t *g_proveOwnershipKb = NULL; // gen-2 prove-ownership verify modal (same widget as add-wallet)
+static uint16_t g_proveOwnershipSig = SIG_FORGET_PASSWORD_PROVE_OWNERSHIP;
+static bool g_proveOwnershipDone = false; // ownership already proven this flow (don't re-pop)
static void CloseCurrentParentAndCloseViewHandler(lv_event_t *e);
+// gen-2 with multiple wallets must prove ownership of another wallet before re-provisioning the forgotten one.
+// gen-1 and single-wallet gen-2 skip the prove-ownership step.
+static bool ForgetPassNeedsProveOwnership(void)
+{
+ uint8_t accountNum = 0;
+ GetExistAccountNum(&accountNum);
+ return (GetSeGen() == SE_GEN_2 && accountNum >= 2);
+}
+
bool GuiIsForgetPass(void)
{
if (g_isForgetPass) {
@@ -83,6 +98,12 @@ static void ContinueStopCreateHandler(lv_event_t *e)
{
g_forgetMkb->currentSlice = 0;
SetNavBarRightBtn(g_pageWidget->navBarWidget, NVS_RIGHT_BUTTON_BUTT, NULL, NULL);
+ // Cancel rewinds to the method-select tile WITHOUT tearing the view down, so GuiForgetPassDeInit's reset
+ // never runs. Explicitly drop the provision-recovery arm and clear the proven flag here — otherwise the redo
+ // skips prove-ownership (the !g_proveOwnershipDone gate) and reuses a stale arm.
+ g_proveOwnershipDone = false;
+ AbandonProvisionRecovery();
+ SetPageLockScreen(true);
CloseToTargetTileView(g_forgetPassTileView.currentTile, FORGET_PASSWORD_METHOD_SELECT);
GUI_DEL_OBJ(g_noticeWindow)
}
@@ -341,9 +362,73 @@ void GuiForgetPassEnterMnemonic(void *parent)
g_enterMnemonicCont = parent;
}
+static void ProveOwnershipCloseModal(void)
+{
+ if (g_proveOwnershipKb != NULL) {
+ GuiDeleteKeyboardWidget(g_proveOwnershipKb);
+ g_proveOwnershipKb = NULL;
+ }
+}
+
+static void ProveOwnershipBackHandler(lv_event_t *e)
+{
+ // back out of the modal -> drop the provision-recovery arm, restore the auto-lock, stay on the mnemonic tile.
+ ProveOwnershipCloseModal();
+ AbandonProvisionRecovery();
+ SetPageLockScreen(true);
+}
+
+// gen-2 multi-wallet "Prove Device Ownership": pop the SAME KeyboardWidget modal as the add-wallet / change-PIN
+// verify (title + desc + PIN/password toggle + GuiShowErrorNumber "N attempts left"), over the forget-pass
+// content, with its "Forgot?" shortcut suppressed for this sig. The verify is wired to
+// SIG_FORGET_PASSWORD_PROVE_OWNERSHIP (see ModelVerifyAccountPass), which arms provision-recovery on the matched
+// wallet; on PASS the flow advances to set the new PIN.
+static void ForgetPassPopProveOwnership(void)
+{
+ // Parent to the page root (parent of the tileview content), NOT the content zone, so the modal covers the
+ // nav bar too — otherwise the underlying forget-pass nav bar's close button shows above the modal's own back
+ // button (two stacked buttons). Mirrors add-wallet's GuiSettingGetCurrentCont() == parent of the content.
+ // Parent to the TOP LAYER, not the forget-pass page root. It still covers the nav bar (single back button),
+ // but its lifecycle is now independent of the page widget — so if the view is torn down with the modal still
+ // open (a FAIL/abort path that never reaches the create view), GuiDeleteKeyboardWidget fully owns its objects
+ // and can't race DestroyPageWidget freeing the same page-root subtree (double-free / heap corruption).
+ g_proveOwnershipKb = GuiCreateKeyboardWidgetView(lv_layer_top(), ProveOwnershipBackHandler, &g_proveOwnershipSig);
+ SetKeyboardWidgetSelf(g_proveOwnershipKb, &g_proveOwnershipKb);
+ SetKeyboardWidgetSig(g_proveOwnershipKb, &g_proveOwnershipSig);
+ SetKeyboardWidgetTitle(g_proveOwnershipKb, _("prove_ownership_title"), _("prove_ownership_desc"));
+}
+
+// Prove-ownership verify result (from the forget-pass view). PASS: ownership proven, provision-recovery already
+// armed in ModelVerifyAccountPass -> advance to set the new PIN. FAIL: mark the entry wrong (red LEDs +
+// "incorrect"), matching the add-wallet pure-check behavior.
+void GuiForgetProveOwnershipResult(bool pass, void *param)
+{
+ if (pass) {
+ // ownership proven (provision-recovery armed): close the modal, then advance the flow to set the new PIN.
+ // The 'done' flag stops ENTER_MNEMONIC's next from re-popping the modal.
+ g_proveOwnershipDone = true;
+ ProveOwnershipCloseModal();
+ GuiForgetPassNextTile(0);
+ return;
+ }
+ if (g_proveOwnershipKb != NULL && param != NULL) {
+ // GuiShowErrorNumber detects the prove-ownership signal and shows "N attempts left" (login-count); at
+ // MAX_LOGIN it opens the recoverable wipe / restore-from-seed view. The modal's self-pointer NULLs
+ // g_proveOwnershipKb on delete.
+ GuiShowErrorNumber(g_proveOwnershipKb, (PasswordVerifyResult_t *)param);
+ }
+}
+
void GuiForgetPassInit(void *param)
{
g_prevView = param;
+ // Fresh flow: never inherit a prior run's "ownership already proven" state. The flag is otherwise only
+ // cleared in GuiForgetPassDeInit / GuiForgetPassPrevTile(SETPIN); resetting it at entry makes each forget-
+ // pass run start from a known state regardless of how the previous one ended, so a multi-wallet run can't
+ // skip the Prove-Ownership gate on a stale true. No-op for single-wallet/gen-1 (ForgetPassNeedsProveOwnership
+ // gates the flag out there).
+ g_proveOwnershipDone = false;
+ GuiResetCheckPasswordCounter(); // fresh per-flow CheckPasswordExisted budget
g_pageWidget = CreatePageWidget();
lv_obj_t *cont = g_pageWidget->contentZone;
@@ -387,6 +472,15 @@ void GuiForgetPassDeInit(void)
GuiDelEnterPasscode(g_repeatPassCode, NULL);
g_repeatPassCode = NULL;
}
+ if (g_proveOwnershipKb != NULL) {
+ GuiDeleteKeyboardWidget(g_proveOwnershipKb);
+ g_proveOwnershipKb = NULL;
+ }
+ // forget-pass torn down: drop any gen-2 provision-recovery arm not yet consumed by the re-save, and restore
+ // the auto-lock that the prove-ownership step may have disabled.
+ AbandonProvisionRecovery();
+ SetPageLockScreen(true);
+ g_proveOwnershipDone = false;
GUI_DEL_OBJ(g_forgetPassTileView.cont)
GuiSettingCloseSelectAmountHintBox();
@@ -437,7 +531,16 @@ int8_t GuiForgetPassNextTile(uint8_t tileIndex)
case FORGET_PASSWORD_ENTER_MNEMONIC:
SetNavBarLeftBtn(g_pageWidget->navBarWidget, NVS_BAR_CLOSE, StopCreateViewHandler, NULL);
lv_obj_add_flag(g_nextCont, LV_OBJ_FLAG_HIDDEN);
- break;
+ if (ForgetPassNeedsProveOwnership() && !g_proveOwnershipDone) {
+ // multi-wallet gen-2: pop the Prove-Ownership modal (same KeyboardWidget as change-PIN's verify) over
+ // this tile instead of advancing. Hold the auto-lock off through the re-save so the provision-recovery
+ // arm survives until provision consumes it. On PASS the modal advances the flow; on back it disarms
+ // and stays here.
+ SetPageLockScreen(false);
+ ForgetPassPopProveOwnership();
+ return SUCCESS_CODE; // stay on ENTER_MNEMONIC behind the modal
+ }
+ break; // single-wallet / gen-1, or ownership already proven -> advance to SETPIN
case FORGET_PASSWORD_SETPIN:
SetNavBarLeftBtn(g_pageWidget->navBarWidget, NVS_BAR_RETURN, ReturnHandler, NULL);
if (g_repeatPassCode == NULL) {
@@ -467,6 +570,13 @@ int8_t GuiForgetPassPrevTile(uint8_t tileIndex)
SetNavBarMidBtn(g_pageWidget->navBarWidget, NVS_MID_BUTTON_BUTT, NULL, NULL);
SetRightBtnLabel(g_pageWidget->navBarWidget, NVS_BAR_WORD_RESET, _("import_wallet_phrase_clear_btn"));
SetRightBtnCb(g_pageWidget->navBarWidget, ResetClearImportHandler, NULL);
+ if (g_proveOwnershipDone) {
+ // backing past the proven step -> drop the provision-recovery arm + restore the auto-lock; the
+ // mnemonic re-entry would re-pop the prove-ownership modal.
+ AbandonProvisionRecovery();
+ SetPageLockScreen(true);
+ g_proveOwnershipDone = false;
+ }
break;
case FORGET_PASSWORD_REPEATPIN:
SetNavBarLeftBtn(g_pageWidget->navBarWidget, NVS_BAR_CLOSE, StopCreateViewHandler, NULL);
diff --git a/src/ui/gui_widgets/gui_forget_pass_widgets.h b/src/ui/gui_widgets/gui_forget_pass_widgets.h
index de8e9c5..0f6582c 100644
--- a/src/ui/gui_widgets/gui_forget_pass_widgets.h
+++ b/src/ui/gui_widgets/gui_forget_pass_widgets.h
@@ -12,6 +12,7 @@ void GuiForgetPassRepeatPinPass(const char* buf);
void GuiForgetPassDeInit(void);
void GuiForgetPassResetPass(bool en, int errCode);
void GuiForgetPassVerifyResult(bool en, int errCode);
+void GuiForgetProveOwnershipResult(bool pass, void *param);
void GuiForgetPassUpdateKeyboard(void);
bool GuiIsForgetPass(void);
diff --git a/src/ui/gui_widgets/gui_lock_widgets.c b/src/ui/gui_widgets/gui_lock_widgets.c
index e50470e..f1b6b75 100644
--- a/src/ui/gui_widgets/gui_lock_widgets.c
+++ b/src/ui/gui_widgets/gui_lock_widgets.c
@@ -9,6 +9,7 @@
#include "gui_hintbox.h"
#include "gui_api.h"
#include "keystore.h"
+#include "se_manager.h"
#include "gui_lock_device_widgets.h"
#include "fingerprint_process.h"
#include "device_setting.h"
diff --git a/src/ui/gui_widgets/gui_system_setting_widgets.c b/src/ui/gui_widgets/gui_system_setting_widgets.c
index abb8949..fa20f68 100644
--- a/src/ui/gui_widgets/gui_system_setting_widgets.c
+++ b/src/ui/gui_widgets/gui_system_setting_widgets.c
@@ -17,6 +17,7 @@
#include "gui_lock_widgets.h"
#include "gui_keyboard_hintbox.h"
#include "gui_page.h"
+#include "gui_wipe_device_widgets.h"
static lv_obj_t *g_container;
static lv_obj_t *g_vibrationSw;
@@ -278,6 +279,7 @@ void GuiSystemSettingVerifyPasswordSuccess(void)
{
printf("password is right\n");
GuiDeleteKeyboardWidget(g_keyboardWidget);
+ GuiWipeDeviceSetForced(false);
GuiFrameOpenView(&g_wipeDeviceView);
}
@@ -455,4 +457,4 @@ static void GuiShowChangePermitKeyBoard(lv_event_t * e)
static uint16_t sig = SIG_SETTING_CHANGE_PERMIT_SWITCH;
SetKeyboardWidgetSig(g_keyboardWidget, &sig);
}
-#endif
\ No newline at end of file
+#endif
diff --git a/src/ui/gui_widgets/gui_wipe_device_widgets.c b/src/ui/gui_widgets/gui_wipe_device_widgets.c
index 1cee6ad..89627e2 100644
--- a/src/ui/gui_widgets/gui_wipe_device_widgets.c
+++ b/src/ui/gui_widgets/gui_wipe_device_widgets.c
@@ -15,6 +15,8 @@ static lv_obj_t *g_cont;
static lv_obj_t *g_wipeDeviceHintBox = NULL;
static lv_timer_t *g_countDownTimer;
static PageWidget_t *g_pageWidget;
+static bool g_forceWipeDevice = false;
+static bool g_wipeDeviceRunning = false;
static void GuiWipeDeviceNVSBarInit();
static void GuiWipeDeviceEntranceWidget(lv_obj_t *parent);
@@ -26,8 +28,19 @@ static void WipeDeviceDeal(void);
static void CountDownTimerHandler(lv_timer_t *timer);
static void GuiCountDownDestruct(void *obj, void* param);
+void GuiWipeDeviceSetForced(bool forced)
+{
+ g_forceWipeDevice = forced;
+ printf("wipe-device forced mode set forced=%d\r\n", g_forceWipeDevice);
+}
+
void GuiWipeDeviceWidgetsInit()
{
+ g_wipeDeviceRunning = false;
+ if (g_forceWipeDevice) {
+ printf("wipe-device forced mode init: disable page lock\r\n");
+ SetPageLockScreen(false);
+ }
g_pageWidget = CreatePageWidget();
lv_obj_t *cont = g_pageWidget->contentZone;
g_cont = cont;
@@ -39,6 +52,7 @@ void GuiWipeDeviceWidgetsInit()
void GuiWipeDeviceWidgetsDeInit()
{
+ GuiCountDownDestruct(NULL, NULL);
GUI_DEL_OBJ(g_wipeDeviceHintBox);
if (g_cont != NULL) {
lv_obj_del(g_cont);
@@ -48,6 +62,11 @@ void GuiWipeDeviceWidgetsDeInit()
DestroyPageWidget(g_pageWidget);
g_pageWidget = NULL;
}
+ if (g_forceWipeDevice && !g_wipeDeviceRunning) {
+ printf("wipe-device forced mode deinit before wipe: restore page lock\r\n");
+ SetPageLockScreen(true);
+ g_forceWipeDevice = false;
+ }
}
void GuiWipeDeviceWidgetsRefresh()
@@ -60,7 +79,11 @@ void GuiWipeDeviceWidgetsRestart()
static void GuiWipeDeviceNVSBarInit()
{
- SetNavBarLeftBtn(g_pageWidget->navBarWidget, NVS_BAR_RETURN, CloseCurrentViewHandler, NULL);
+ if (g_forceWipeDevice) {
+ SetNavBarLeftBtn(g_pageWidget->navBarWidget, NVS_LEFT_BUTTON_BUTT, NULL, NULL);
+ } else {
+ SetNavBarLeftBtn(g_pageWidget->navBarWidget, NVS_BAR_RETURN, CloseCurrentViewHandler, NULL);
+ }
}
void GuiWipeDeviceEntranceWidget(lv_obj_t *parent)
@@ -93,7 +116,13 @@ static void GuiShowWipeDeviceHintBox(void)
g_wipeDeviceHintBox = GuiCreateGeneralHintBox(&imgWarn, _("wipe_device"), _("system_settings_wipe_device_wipe_alert_desc"), NULL,
_("not_now"), WHITE_COLOR_OPA20, _("system_settings_wipe_device_wipe_start_text"), ORANGE_COLOR);
lv_obj_t *leftBtn = GuiGetHintBoxLeftBtn(g_wipeDeviceHintBox);
- lv_obj_add_event_cb(leftBtn, NotNowHandler, LV_EVENT_CLICKED, NULL);
+ if (g_forceWipeDevice) {
+ printf("wipe-device forced mode: hide not-now button\r\n");
+ lv_obj_add_flag(leftBtn, LV_OBJ_FLAG_HIDDEN);
+ lv_obj_clear_flag(leftBtn, LV_OBJ_FLAG_CLICKABLE);
+ } else {
+ lv_obj_add_event_cb(leftBtn, NotNowHandler, LV_EVENT_CLICKED, NULL);
+ }
lv_obj_t *rightBtn = GuiGetHintBoxRightBtn(g_wipeDeviceHintBox);
lv_obj_add_event_cb(rightBtn, ExecWipeDeviceHandler, LV_EVENT_CLICKED, NULL);
lv_obj_clear_flag(rightBtn, LV_OBJ_FLAG_CLICKABLE);
@@ -116,6 +145,8 @@ static void ExecWipeDeviceHandler(lv_event_t *e)
static void WipeDeviceDeal(void)
{
+ g_wipeDeviceRunning = true;
+ printf("wipe-device start wipe forced=%d\r\n", g_forceWipeDevice);
if (g_cont != NULL) {
lv_obj_del(g_cont);
g_cont = NULL;
@@ -173,4 +204,4 @@ static void GuiCountDownDestruct(void *obj, void* param)
lv_timer_del(g_countDownTimer);
g_countDownTimer = NULL;
}
-}
\ No newline at end of file
+}
diff --git a/src/ui/gui_widgets/gui_wipe_device_widgets.h b/src/ui/gui_widgets/gui_wipe_device_widgets.h
index 7cb577a..9345fb3 100644
--- a/src/ui/gui_widgets/gui_wipe_device_widgets.h
+++ b/src/ui/gui_widgets/gui_wipe_device_widgets.h
@@ -1,9 +1,12 @@
#ifndef _GUI_WIPE_DEVICE_WIDGETS_H
#define _GUI_WIPE_DEVICE_WIDGETS_H
+#include <stdbool.h>
+
void GuiWipeDeviceWidgetsInit();
void GuiWipeDeviceWidgetsDeInit();
void GuiWipeDeviceWidgetsRefresh();
void GuiWipeDeviceWidgetsRestart();
+void GuiWipeDeviceSetForced(bool forced);
-#endif
\ No newline at end of file
+#endif
diff --git a/src/ui/gui_widgets/setting/gui_setting_widgets.c b/src/ui/gui_widgets/setting/gui_setting_widgets.c
index 0270a96..9c3b6a7 100644
--- a/src/ui/gui_widgets/setting/gui_setting_widgets.c
+++ b/src/ui/gui_widgets/setting/gui_setting_widgets.c
@@ -75,6 +75,7 @@ void WalletSettingHandler(lv_event_t *e)
static void CloseToFingerAndPassView(lv_event_t *e)
{
GUI_DEL_OBJ(g_noticeWindow)
+ SetPageLockScreen(true); // change-password flow returned to the sub-menu: re-arm the auto-lock
for (int i = g_deviceSetTileView.currentTile; i > 2; i--) {
GuiEmitSignal(SIG_SETUP_VIEW_TILE_PREV, NULL, 0);
}
@@ -392,12 +393,20 @@ void GuiDevSettingPassCode(bool result, uint16_t tileIndex)
}
break;
case SIG_SETTING_CHANGE_PASSWORD:
+ // Hold the auto-lock off across the change-password flow (verify old PIN -> set new -> re-wrap), so an
+ // inactivity lock mid-flow can't tear the view down and interrupt the gen-2 re-wrap. Re-enabled on the
+ // success return (CloseToFingerAndPassView) and as a catch-all in GuiSettingDeInit.
+ SetPageLockScreen(false);
+ GuiResetCheckPasswordCounter(); // fresh per-flow CheckPasswordExisted budget
walletIndex = DEVICE_SETTING_RESET_PASSCODE_VERIFY;
break;
case DEVICE_SETTING_PASSPHRASE_VERIFY:
walletIndex = DEVICE_SETTING_PASSPHRASE_ENTER;
break;
case DEVICE_SETTING_ADD_WALLET:
+ // Limit already gated at the button (GuiAddWalletEntryHandler); PIN verified here. Land on the notice,
+ // which is the add-wallet operation entry: it disables the auto-lock and disarms the gen-2 R-recovery
+ // holder on back. The arm fired during this verify is consumed later by the create-wallet provision.
ClearSecretCache();
walletIndex = DEVICE_SETTING_ADD_WALLET_NOTICE;
break;
@@ -450,6 +459,7 @@ void GuiSettingInit(void)
void GuiSettingDeInit(void)
{
+ SetPageLockScreen(true); // catch-all: leaving settings always re-arms the auto-lock (change-password flow)
GuiShowKeyboardDestruct();
GUI_DEL_OBJ(g_noticeWindow)
GUI_DEL_OBJ(g_selectAmountHintbox)
diff --git a/src/ui/gui_widgets/setting/gui_wallet_setting_widgets.c b/src/ui/gui_widgets/setting/gui_wallet_setting_widgets.c
index 6f706fe..c2eb1fa 100644
--- a/src/ui/gui_widgets/setting/gui_wallet_setting_widgets.c
+++ b/src/ui/gui_widgets/setting/gui_wallet_setting_widgets.c
@@ -20,6 +20,7 @@
#include "account_manager.h"
#include "gui_lock_widgets.h"
#include "screen_manager.h"
+#include "se_manager.h" // SE_DisarmProvisionRecovery for gen-2 add-wallet
#include "fingerprint_process.h"
#include "keystore.h"
#include "gui_home_widgets.h"
@@ -173,6 +174,11 @@ void GuiWalletAddWalletNotice(lv_obj_t *parent)
{
uint16_t height;
static uint32_t walletSetting = DEVICE_SETTING_ADD_WALLET_CREATE_OR_IMPORT;
+ // Notice is the add-wallet operation entry (PIN already verified at the button). Hold the auto-lock off for
+ // the rest of the flow; the notice tile's destruct (GuiSettingCountDownDestruct) re-enables it and disarms
+ // the gen-2 provision recovery if the user backs out.
+ SetPageLockScreen(false);
+ GuiResetCheckPasswordCounter(); // reset per-flow CheckPasswordExisted counter
lv_obj_set_style_bg_opa(parent, LV_OPA_0, LV_PART_SCROLLBAR | LV_STATE_SCROLLED);
lv_obj_set_style_bg_opa(parent, LV_OPA_0, LV_PART_SCROLLBAR | LV_STATE_DEFAULT);
lv_obj_t *label = GuiCreateTitleLabel(parent, _("wallet_settings_add_info_title"));
@@ -226,6 +232,12 @@ void GuiSettingCountDownDestruct(void *obj, void *param)
lv_timer_del(g_countDownTimer);
g_countDownTimer = NULL;
}
+ // Notice tile teardown = add-wallet operation boundary. This fires on nav-back (notice tile removed) and on
+ // any deeper teardown, but NOT when navigating forward into create/import (those tiles stack on top, the
+ // notice tile stays). So: drop the gen-2 provision-recovery arm if the user backed out before provisioning
+ // (a no-op once provision has consumed it), and re-enable the auto-lock disabled in GuiWalletAddWalletNotice.
+ AbandonProvisionRecovery();
+ SetPageLockScreen(true);
}
void GuiWalletSetPinWidget(lv_obj_t *parent, uint8_t tile)
@@ -384,6 +396,21 @@ void GuiShowKeyboardHandler(lv_event_t *e)
SetKeyboardWidgetSig(g_keyboardWidget, walletSetIndex);
}
+// Add-wallet entry: gate the wallet-count limit BEFORE the PIN verify. 3 wallets -> limit screen (no verify, so
+// the gen-2 provision-recovery arm never fires on a dead-end); otherwise pop the PIN keyboard. On verify success
+// the flow lands on the notice (the operation entry point), which disables the auto-lock and disarms on back.
+void GuiAddWalletEntryHandler(lv_event_t *e)
+{
+ uint8_t accountNum = 0;
+ GetExistAccountNum(&accountNum);
+ if (accountNum >= 3) {
+ static uint8_t walletIndex = DEVICE_SETTING_ADD_WALLET_LIMIT;
+ GuiEmitSignal(SIG_SETUP_VIEW_TILE_NEXT, &walletIndex, sizeof(walletIndex));
+ } else {
+ GuiShowKeyboardHandler(e); // user_data = DEVICE_SETTING_ADD_WALLET -> verify keyboard
+ }
+}
+
void GuiVerifyCurrentPasswordErrorCount(void *param)
{
PasswordVerifyResult_t *passwordVerifyResult = (PasswordVerifyResult_t *)param;
@@ -512,7 +539,9 @@ void GuiWalletSetWidget(lv_obj_t *parent)
label = GuiCreateTextLabel(parent, _("wallet_setting_add_wallet"));
lv_obj_set_style_text_color(label, ORANGE_COLOR, LV_PART_MAIN);
table[0].obj = label;
- button = GuiCreateButton(parent, 456, 84, table, 1, GuiShowKeyboardHandler, &walletSetting[4]);
+ // Pre-verify limit gate (see GuiAddWalletEntryHandler): no PIN keyboard here anymore; verify is deferred to
+ // the notice's "next" button so the gen-2 provision-recovery arm is bound to the create flow.
+ button = GuiCreateButton(parent, 456, 84, table, 1, GuiAddWalletEntryHandler, &walletSetting[4]);
lv_obj_align(button, LV_ALIGN_DEFAULT, 12, nextY);
}
diff --git a/src/ui/lv_i18n/data.csv b/src/ui/lv_i18n/data.csv
index 603afde..afd51ac 100644
--- a/src/ui/lv_i18n/data.csv
+++ b/src/ui/lv_i18n/data.csv
@@ -555,6 +555,8 @@ Unlock Device,24,unlock_device_fingerprint_pin_title,Use PIN or Fingerprint,PIN
,28,forgot_password_reset_passcode_intro_title,Forgot passcode?,Забыли код-пароль?,비밀번호를 잊어버렸습니까?,忘记密码?,¿Olvidaste tu código de acceso?,Passcode vergessen?,パスコードを忘れましたか?
,24,forgot_password_reset_passcode_intro_text,Forgot passcode?,Забыли код-пароль?,비밀번호를 잊어버렸습니까?,忘记密码?,¿Olvidaste tu código de acceso?,Passcode vergessen?,パスコードを忘れましたか?
,20,forgot_password_reset_passcode_intro_desc,Verify the seed phrase associated with this wallet to reset the passcode.Resetting your password may erase your wallet or multisig wallet data,"Проверьте начальную фразу, связанную с этим кошельком, чтобы сбросить пароль. Сброс пароля может привести к удалению данных вашего кошелька или мультиподписного кошелька",이 지갑과 연결된 구문을 확인하여 암호를 재설정합니다.암호를 재설정하면 지갑이나 다중 서명 지갑의 데이터를 지울 수 있습니다,验证与此钱包关联的助记词以重置密码.重置密码可能会擦除您的钱包或多重签名钱包数据,Verifica la frase semilla asociada a esta billetera para restablecer el código de acceso. Restablecer tu contraseña puede borrar los datos de tu billetera o billetera multisig,"Überprüfen Sie die mit dieser Brieftasche verbundene Wiederherstellungsphrase, um den Zugangscode zurückzusetzen. Das Zurücksetzen Ihres Passworts kann Ihre Brieftasche oder Multisig-Brieftaschendaten löschen.",このウォレットに関連するシードフレーズを確認してパスコードをリセットしてください.パスワードをリセットすると、ウォレットまたはマルチシグウォレットのデータが消去される可能性があります.
+,20,prove_ownership_title,Prove Device Ownership,Подтвердите владение устройством,기기 소유권 증명,验证设备所有权,Demostrar propiedad del dispositivo,Geräteeigentum nachweisen,デバイスの所有権を証明
+,20,prove_ownership_desc,"To continue resetting your passcode, enter the PIN or password of another wallet on this device.","Чтобы продолжить сброс пароля, введите PIN-код или пароль другого кошелька на этом устройстве.",비밀번호 재설정을 계속하려면 이 기기에 있는 다른 지갑의 PIN 또는 비밀번호를 입력하세요.,若要继续重置密码,请输入本设备上另一个钱包的 PIN 码或密码。,"Para continuar restableciendo tu código de acceso, introduce el PIN o la contraseña de otra billetera en este dispositivo.","Um das Zurücksetzen Ihres Passcodes fortzusetzen, geben Sie die PIN oder das Passwort einer anderen Brieftasche auf diesem Gerät ein.",パスコードのリセットを続行するには、このデバイス上の別のウォレットのPINまたはパスワードを入力してください。
,28,forget_password_cancel,Cancel Password Reset?,Отменить сброс пароля?,암호 재설정 취소?,取消密码重置?,¿Cancelar restablecimiento de contraseña?,Passwort zurücksetzen abbrechen?,パスワードリセットをキャンセルしますか?
Error,28,error_box_invalid_seed_phrase,Invalid Seed Phrase,Неверная фраза,잘못된 시드 구문입니다.,助记词无效,Frase semilla no válida,Ungültige Seed-Phrase,無効なシードフレーズ
,20,error_box_invalid_seed_phrase_desc,The seed phrase you've entered is invalid. Please re-verify your backup and try again.,Введенная фраза недействительна. Проверьте ее еще раз и повторите попытку.,입력하신 시드 구문이 잘 못되었습니다. 확인하고 다시 시도하십시오.,"您输入的助记词无效.请检查您所备份的助记词,重新尝试.","La frase semilla que has ingresado es inválida. Por favor, verifica nuevamente tu respaldo e intenta de nuevo.",Der eingegebene Seed-Satz ist ungültig. Bitte überprüfen Sie Ihre Sicherung erneut und versuchen Sie es erneut.,入力したシードフレーズは無効です.バックアップを再確認して、もう一度試してください.
@@ -642,7 +644,7 @@ About Info,24,about_info_firmware_version,Firmware Version,Версия прош
,24,about_info_serial_number,Serial Number,Серийный номер,제조 번호,序列号,Número de serie,Seriennummer,シリアルナンバー
,24,about_info_export_log,Export System Log,Выгрузка логов,시스템 로그 내보내기,导出系统日志,Exportar el registro del sistema,Systemprotokoll exportieren,エクスポートシステムログ
,24,about_info_device_uid,Device UID,IOD устройства,장치 UID,设备UID,UID del dispositivo,Gerät UID,デバイスUID
-,24,about_info_fingerprint_firmware_version,Fingerprint Firmware Version,Отпечаток версии прошивки,지문 펌웨어 버전,指纹固件版本,Versión del firmware de huella dactilar,Fingerabdruck-Firmware-Version,指紋のファームウェアバージョン
+,24,about_info_fingerprint_firmware_version,FPF Version & SE Version,Версия прошивки отпечатка и защищённого чипа,지문 펌웨어 및 보안 칩 버전,指纹固件及安全芯片版本,Versión de firmware de huella y chip seguro,Fingerabdruck-Firmware- und Sicherheitschip-Version,指紋ファームウェア及びセキュアチップのバージョン
,24,about_info_battery_voltage,Battery Voltage,Напряжение батареи,배터리 전압,电池电压,Voltaje de la batería,Batteriespannung,バッテリー電圧
,28,about_info_result_export_successful,Export Successful,Успешная выгрузка,내보내기 성공,导出成功,Exportación exitosa,Export erfolgreich,エクスポートが成功しました.
,20,about_info_result_export_successful_desc,Your system log has been successfully exported to the MicroSD Card.,Системные логи были успешно выгружены на MicroSD карту.,시스템 로그를 MicroSD 카드로 내보냈습니다.,您的系统日志已成功导出到 microSD 卡.,El registro de tu sistema ha sido exportado correctamente a la tarjeta MicroSD,Ihr Systemprotokoll wurde erfolgreich auf die MicroSD-Karte exportiert.,システムログはMicroSDカードに正常にエクスポートされました.
@@ -732,6 +734,9 @@ password,20,password_error_too_short,Password must be at least 6 characters,Па
,24,password_input_desc,Enter your password,Введите пароль,당신의 암호를 입력하세요.,输入您的密码,Ingresa tu contraseña,Geben Sie Ihr Passwort ein.,パスワードを入力してください.
,20,password_error_duplicated_pincode,Duplicate PIN code detected. Please use a different one.,Этот PIN-код уже используется. Используйте другой.,중복된 PIN 코드가 감지되었습니다. 다른 PIN 코드를 사용하십시오.,检测到重复的PIN码.请设置新PIN码.,"Código PIN duplicado detectado. Por favor, utiliza uno distinto.",Doppelter PIN-Code erkannt. Bitte verwenden Sie einen anderen.,重複したPINコードが検出されました.別のコードを使用してください.
,24,password_error_too_weak,Set a strong password,Установите сложный пароль,강력한 암호 설정 필요합니다. ,设置强密码,Establecer una contraseña segura,Starkes Passwort wählen,強力なパスワード設定
+,24,weak_passcode_warning_title,Weak PIN or Password,Слабый PIN-код или пароль,취약한 PIN 또는 비밀번호,弱PIN码或密码,PIN o contraseña débil,Schwache PIN oder schwaches Passwort,脆弱なPINまたはパスワード
+,20,weak_passcode_warning_desc,This PIN or password is easy to guess. Change it to a stronger one?,Этот PIN-код или пароль легко угадать. Изменить на более надежный?,이 PIN 또는 비밀번호는 추측하기 쉽습니다. 더 강력한 것으로 변경하시겠습니까?,此PIN码或密码容易被猜到。要改为更强的吗?,Este PIN o contraseña es fácil de adivinar. ¿Cambiarlo por uno más seguro?,Diese PIN oder dieses Passwort ist leicht zu erraten. In eine stärkere ändern?,このPINまたはパスワードは推測されやすいです。より強力なものに変更しますか?
+,20,weak_passcode_warning_change,Change it,Изменить,변경,更改,Cambiar,Ändern,変更
,20,password_error_too_long,The input cannot exceed 128 characters,Пароль не может превышать 128 символов.,입력은 128자를 초과할 수 없습니다,输入不能超过128个字符,El texto ingresado no puede exceder los 128 caracteres,Die Eingabe darf 128 Zeichen nicht überschreiten.,入力は128文字を超えることはできません.
,20,password_error_fingerprint_attempts_exceed,Too many attempts. Please enter your passcode to unlock the device.,"Слишком много попыток. Введите код-пароль, чтобы разблокировать устройство.",시도 횟수가 너무 많습니다. 장치 잠금을 해제하려면 암호를 입력하십시오.,尝试次数过多.请输入您的密码以解锁设备.,"Demasiados intentos. Por favor, introduce tu código de acceso para desbloquear el dispositivo.","Zu viele Versuche. Bitte geben Sie Ihren Zugangscode ein, um das Gerät zu entsperren.",試行回数が多すぎます.デバイスをロック解除するためにパスコードを入力してください.
transaction parse,20,transaction_parse_confirm_message,Confirm Message,Подтвердить сообщение,메시지 확인,确认消息,Confirmar Mensaje,Bestätigungsnachricht,確認メッセージ
diff --git a/src/ui/lv_i18n/lv_i18n.c b/src/ui/lv_i18n/lv_i18n.c
index a3442d6..0fc04b9 100644
--- a/src/ui/lv_i18n/lv_i18n.c
+++ b/src/ui/lv_i18n/lv_i18n.c
@@ -86,7 +86,7 @@ const static lv_i18n_phrase_t en_singulars[] = {
{"about_info_export_file_name", "File name:"},
{"about_info_export_log", "Export System Log"},
{"about_info_export_to_sdcard", "Export Log to MicroSD Card"},
- {"about_info_fingerprint_firmware_version", "Fingerprint Firmware Version"},
+ {"about_info_fingerprint_firmware_version", "FPF Version & SE Version"},
{"about_info_firmware_version", "Firmware Version"},
{"about_info_firmware_version_head", "Firmware"},
{"about_info_result_export_failed", "Export Failed"},
@@ -456,6 +456,8 @@ const static lv_i18n_phrase_t en_singulars[] = {
{"firmware_update_via_usb", "Via USB"},
{"forget_password_cancel", "Cancel Password Reset?"},
{"forgot_password_reset_passcode_intro_desc", "Verify the seed phrase associated with this wallet to reset the passcode.Resetting your password may erase your wallet or multisig wallet data"},
+ {"prove_ownership_title", "Prove Device Ownership"},
+ {"prove_ownership_desc", "To continue resetting your passcode, enter the PIN or password of another wallet on this device."},
{"forgot_password_reset_passcode_intro_text", "Forgot passcode?"},
{"forgot_password_reset_passcode_intro_title", "Forgot passcode?"},
{"generating_qr_codes", "Generating QR Codes"},
@@ -600,6 +602,9 @@ const static lv_i18n_phrase_t en_singulars[] = {
{"password_error_too_long", "The input cannot exceed 128 characters"},
{"password_error_too_short", "Password must be at least 6 characters"},
{"password_error_too_weak", "Set a strong password"},
+ {"weak_passcode_warning_change", "Change it"},
+ {"weak_passcode_warning_desc", "This PIN or password is easy to guess. Change it to a stronger one?"},
+ {"weak_passcode_warning_title", "Weak PIN or Password"},
{"password_input_desc", "Enter your password"},
{"password_label", "PASSWORD"},
{"password_score_good", "Good"},
@@ -1045,7 +1050,7 @@ const static lv_i18n_phrase_t de_singulars[] = {
{"about_info_export_file_name", "Dateiname:"},
{"about_info_export_log", "Systemprotokoll exportieren"},
{"about_info_export_to_sdcard", "Exportieren Sie das Protokoll auf eine MicroSD-Karte."},
- {"about_info_fingerprint_firmware_version", "Fingerabdruck-Firmware-Version"},
+ {"about_info_fingerprint_firmware_version", "Fingerabdruck-Firmware- und Sicherheitschip-Version"},
{"about_info_firmware_version", "Firmware-Version"},
{"about_info_firmware_version_head", "Firmware"},
{"about_info_result_export_failed", "Export fehlgeschlagen"},
@@ -1415,6 +1420,8 @@ const static lv_i18n_phrase_t de_singulars[] = {
{"firmware_update_via_usb", "Via USB"},
{"forget_password_cancel", "Passwort zurücksetzen abbrechen?"},
{"forgot_password_reset_passcode_intro_desc", "Überprüfen Sie die mit dieser Brieftasche verbundene Wiederherstellungsphrase, um den Zugangscode zurückzusetzen. Das Zurücksetzen Ihres Passworts kann Ihre Brieftasche oder Multisig-Brieftaschendaten löschen."},
+ {"prove_ownership_title", "Geräteeigentum nachweisen"},
+ {"prove_ownership_desc", "Um das Zurücksetzen Ihres Passcodes fortzusetzen, geben Sie die PIN oder das Passwort einer anderen Brieftasche auf diesem Gerät ein."},
{"forgot_password_reset_passcode_intro_text", "Passcode vergessen?"},
{"forgot_password_reset_passcode_intro_title", "Passcode vergessen?"},
{"generating_qr_codes", "Generieren von QR-Codes"},
@@ -1559,6 +1566,9 @@ const static lv_i18n_phrase_t de_singulars[] = {
{"password_error_too_long", "Die Eingabe darf 128 Zeichen nicht überschreiten."},
{"password_error_too_short", "Das Passwort muss mindestens 6 Zeichen lang sein."},
{"password_error_too_weak", "Starkes Passwort wählen"},
+ {"weak_passcode_warning_change", "Ändern"},
+ {"weak_passcode_warning_desc", "Diese PIN oder dieses Passwort ist leicht zu erraten. In eine stärkere ändern?"},
+ {"weak_passcode_warning_title", "Schwache PIN oder schwaches Passwort"},
{"password_input_desc", "Geben Sie Ihr Passwort ein."},
{"password_label", "KENNWORT"},
{"password_score_good", "Gut"},
@@ -2004,7 +2014,7 @@ const static lv_i18n_phrase_t es_singulars[] = {
{"about_info_export_file_name", "Nombre de archivo:"},
{"about_info_export_log", "Exportar el registro del sistema"},
{"about_info_export_to_sdcard", "Exportar registro a tarjeta MicroSD"},
- {"about_info_fingerprint_firmware_version", "Versión del firmware de huella dactilar"},
+ {"about_info_fingerprint_firmware_version", "Versión de firmware de huella y chip seguro"},
{"about_info_firmware_version", "Versión de firmware"},
{"about_info_firmware_version_head", "Firmware"},
{"about_info_result_export_failed", "Exportación fallida"},
@@ -2374,6 +2384,8 @@ const static lv_i18n_phrase_t es_singulars[] = {
{"firmware_update_via_usb", "A través de USB"},
{"forget_password_cancel", "¿Cancelar restablecimiento de contraseña?"},
{"forgot_password_reset_passcode_intro_desc", "Verifica la frase semilla asociada a esta billetera para restablecer el código de acceso. Restablecer tu contraseña puede borrar los datos de tu billetera o billetera multisig"},
+ {"prove_ownership_title", "Demostrar propiedad del dispositivo"},
+ {"prove_ownership_desc", "Para continuar restableciendo tu código de acceso, introduce el PIN o la contraseña de otra billetera en este dispositivo."},
{"forgot_password_reset_passcode_intro_text", "¿Olvidaste tu código de acceso?"},
{"forgot_password_reset_passcode_intro_title", "¿Olvidaste tu código de acceso?"},
{"generating_qr_codes", "Generando códigos QR"},
@@ -2518,6 +2530,9 @@ const static lv_i18n_phrase_t es_singulars[] = {
{"password_error_too_long", "El texto ingresado no puede exceder los 128 caracteres"},
{"password_error_too_short", "La contraseña debe tener al menos 6 caracteres"},
{"password_error_too_weak", "Establecer una contraseña segura"},
+ {"weak_passcode_warning_change", "Cambiar"},
+ {"weak_passcode_warning_desc", "Este PIN o contraseña es fácil de adivinar. ¿Cambiarlo por uno más seguro?"},
+ {"weak_passcode_warning_title", "PIN o contraseña débil"},
{"password_input_desc", "Ingresa tu contraseña"},
{"password_label", "CONTRASEÑA"},
{"password_score_good", "Bueno"},
@@ -2960,7 +2975,7 @@ const static lv_i18n_phrase_t ja_singulars[] = {
{"about_info_export_file_name", "ファイル名:"},
{"about_info_export_log", "エクスポートシステムログ"},
{"about_info_export_to_sdcard", "マイクロSDカードにログをエクスポート"},
- {"about_info_fingerprint_firmware_version", "指紋のファームウェアバージョン"},
+ {"about_info_fingerprint_firmware_version", "指紋ファームウェア及びセキュアチップのバージョン"},
{"about_info_firmware_version", "ファームウェアバージョン"},
{"about_info_firmware_version_head", "ファーム"},
{"about_info_result_export_failed", "エクスポートに失敗しました."},
@@ -3330,6 +3345,8 @@ const static lv_i18n_phrase_t ja_singulars[] = {
{"firmware_update_via_usb", "USB経由で"},
{"forget_password_cancel", "パスワードリセットをキャンセルしますか?"},
{"forgot_password_reset_passcode_intro_desc", "このウォレットに関連するシードフレーズを確認してパスコードをリセットしてください.パスワードをリセットすると、ウォレットまたはマルチシグウォレットのデータが消去される可能性があります."},
+ {"prove_ownership_title", "デバイスの所有権を証明"},
+ {"prove_ownership_desc", "パスコードのリセットを続行するには、このデバイス上の別のウォレットのPINまたはパスワードを入力してください。"},
{"forgot_password_reset_passcode_intro_text", "パスコードを忘れましたか?"},
{"forgot_password_reset_passcode_intro_title", "パスコードを忘れましたか?"},
{"generating_qr_codes", "QRコードの生成"},
@@ -3474,6 +3491,9 @@ const static lv_i18n_phrase_t ja_singulars[] = {
{"password_error_too_long", "入力は128文字を超えることはできません."},
{"password_error_too_short", "パスワードは最低6文字以上必要です."},
{"password_error_too_weak", "強力なパスワード設定"},
+ {"weak_passcode_warning_change", "変更"},
+ {"weak_passcode_warning_desc", "このPINまたはパスワードは推測されやすいです。より強力なものに変更しますか?"},
+ {"weak_passcode_warning_title", "脆弱なPINまたはパスワード"},
{"password_input_desc", "パスワードを入力してください."},
{"password_label", "パスワード"},
{"password_score_good", "良い"},
@@ -3914,7 +3934,7 @@ const static lv_i18n_phrase_t ko_singulars[] = {
{"about_info_export_file_name", "파일명"},
{"about_info_export_log", "시스템 로그 내보내기"},
{"about_info_export_to_sdcard", "MicroSD 카드로 로그 내보내기"},
- {"about_info_fingerprint_firmware_version", "지문 펌웨어 버전"},
+ {"about_info_fingerprint_firmware_version", "지문 펌웨어 및 보안 칩 버전"},
{"about_info_firmware_version", "펌웨어 버전"},
{"about_info_firmware_version_head", "펌웨어"},
{"about_info_result_export_failed", "내보내기 실패"},
@@ -4284,6 +4304,8 @@ const static lv_i18n_phrase_t ko_singulars[] = {
{"firmware_update_via_usb", "USB"},
{"forget_password_cancel", "암호 재설정 취소?"},
{"forgot_password_reset_passcode_intro_desc", "이 지갑과 연결된 구문을 확인하여 암호를 재설정합니다.암호를 재설정하면 지갑이나 다중 서명 지갑의 데이터를 지울 수 있습니다"},
+ {"prove_ownership_title", "기기 소유권 증명"},
+ {"prove_ownership_desc", "비밀번호 재설정을 계속하려면 이 기기에 있는 다른 지갑의 PIN 또는 비밀번호를 입력하세요."},
{"forgot_password_reset_passcode_intro_text", "비밀번호를 잊어버렸습니까?"},
{"forgot_password_reset_passcode_intro_title", "비밀번호를 잊어버렸습니까?"},
{"generating_qr_codes", "QR 코드 생성"},
@@ -4428,6 +4450,9 @@ const static lv_i18n_phrase_t ko_singulars[] = {
{"password_error_too_long", "입력은 128자를 초과할 수 없습니다"},
{"password_error_too_short", "비밀번호는 6자 이상이어야 합니다."},
{"password_error_too_weak", "강력한 암호 설정 필요합니다. "},
+ {"weak_passcode_warning_change", "변경"},
+ {"weak_passcode_warning_desc", "이 PIN 또는 비밀번호는 추측하기 쉽습니다. 더 강력한 것으로 변경하시겠습니까?"},
+ {"weak_passcode_warning_title", "취약한 PIN 또는 비밀번호"},
{"password_input_desc", "당신의 암호를 입력하세요."},
{"password_label", "비밀번호"},
{"password_score_good", "암호에 강함"},
@@ -4868,7 +4893,7 @@ const static lv_i18n_phrase_t ru_singulars[] = {
{"about_info_export_file_name", "прошивкой Название:"},
{"about_info_export_log", "Выгрузка логов"},
{"about_info_export_to_sdcard", "Выгрузить логи на MicroSD"},
- {"about_info_fingerprint_firmware_version", "Отпечаток версии прошивки"},
+ {"about_info_fingerprint_firmware_version", "Версия прошивки отпечатка и защищённого чипа"},
{"about_info_firmware_version", "Версия прошивки"},
{"about_info_firmware_version_head", "Прошивка"},
{"about_info_result_export_failed", "Ошибка выгрузки"},
@@ -5238,6 +5263,8 @@ const static lv_i18n_phrase_t ru_singulars[] = {
{"firmware_update_via_usb", "Через USB кабель"},
{"forget_password_cancel", "Отменить сброс пароля?"},
{"forgot_password_reset_passcode_intro_desc", "Проверьте начальную фразу, связанную с этим кошельком, чтобы сбросить пароль. Сброс пароля может привести к удалению данных вашего кошелька или мультиподписного кошелька"},
+ {"prove_ownership_title", "Подтвердите владение устройством"},
+ {"prove_ownership_desc", "Чтобы продолжить сброс пароля, введите PIN-код или пароль другого кошелька на этом устройстве."},
{"forgot_password_reset_passcode_intro_text", "Забыли код-пароль?"},
{"forgot_password_reset_passcode_intro_title", "Забыли код-пароль?"},
{"generating_qr_codes", "Генерируются QR-коды"},
@@ -5382,6 +5409,9 @@ const static lv_i18n_phrase_t ru_singulars[] = {
{"password_error_too_long", "Пароль не может превышать 128 символов."},
{"password_error_too_short", "Пароль должен состоять минимум из 6 символов"},
{"password_error_too_weak", "Установите сложный пароль"},
+ {"weak_passcode_warning_change", "Изменить"},
+ {"weak_passcode_warning_desc", "Этот PIN-код или пароль легко угадать. Изменить на более надежный?"},
+ {"weak_passcode_warning_title", "Слабый PIN-код или пароль"},
{"password_input_desc", "Введите пароль"},
{"password_label", "ПАРОЛЬ"},
{"password_score_good", "Хороший"},
@@ -5830,7 +5860,7 @@ const static lv_i18n_phrase_t zh_cn_singulars[] = {
{"about_info_export_file_name", "文件名"},
{"about_info_export_log", "导出系统日志"},
{"about_info_export_to_sdcard", "将日志导出到 microSD卡"},
- {"about_info_fingerprint_firmware_version", "指纹固件版本"},
+ {"about_info_fingerprint_firmware_version", "指纹固件及安全芯片版本"},
{"about_info_firmware_version", "固件版本"},
{"about_info_firmware_version_head", "固件"},
{"about_info_result_export_failed", "导出失败"},
@@ -6200,6 +6230,8 @@ const static lv_i18n_phrase_t zh_cn_singulars[] = {
{"firmware_update_via_usb", "USB"},
{"forget_password_cancel", "取消密码重置?"},
{"forgot_password_reset_passcode_intro_desc", "验证与此钱包关联的助记词以重置密码.重置密码可能会擦除您的钱包或多重签名钱包数据"},
+ {"prove_ownership_title", "验证设备所有权"},
+ {"prove_ownership_desc", "若要继续重置密码,请输入本设备上另一个钱包的 PIN 码或密码。"},
{"forgot_password_reset_passcode_intro_text", "忘记密码?"},
{"forgot_password_reset_passcode_intro_title", "忘记密码?"},
{"generating_qr_codes", "生成二维码"},
@@ -6344,6 +6376,9 @@ const static lv_i18n_phrase_t zh_cn_singulars[] = {
{"password_error_too_long", "输入不能超过128个字符"},
{"password_error_too_short", "密码必须至少 6 个字符"},
{"password_error_too_weak", "设置强密码"},
+ {"weak_passcode_warning_change", "更改"},
+ {"weak_passcode_warning_desc", "此PIN码或密码容易被猜到。要改为更强的吗?"},
+ {"weak_passcode_warning_title", "弱PIN码或密码"},
{"password_input_desc", "输入您的密码"},
{"password_label", "密码"},
{"password_score_good", "密码强"},
diff --git a/ui_simulator/simulator_model.c b/ui_simulator/simulator_model.c
index 553f5ee..772bfb7 100644
--- a/ui_simulator/simulator_model.c
+++ b/ui_simulator/simulator_model.c
@@ -1,7 +1,9 @@
#include "simulator_model.h"
#include "librust_c.h"
#include "gui.h"
+#include "gui_framework.h"
#include "gui_home_widgets.h"
+#include "gui_views.h"
#include "cjson/cJSON.h"
#include "stdint.h"
#include "gui_resolve_ur.h"
@@ -713,4 +715,4 @@ int32_t read_qrcode()
bool GetEnsName(const char *addr, char *name)
{
return false;
-}
\ No newline at end of file
+}
Why this scored 59/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.