logic/pin(纯 C):加盐迭代哈希(FNV-1a ×1000 轮 + 每次设置新随机盐,16 位 hex)经
meow_config_* 持久化 pin.enabled/pin.hash/pin.salt;提供 set/verify/change/disable 与
4-8 位纯数字校验;enabled=1 但盐/哈希残缺时 fail-closed;落盘半途失败会还原旧盐,不留
「新盐+旧哈希」的双失效状态。
ui/settings(ACT_SETTINGS):MD3 分组表单(meow_listview 容器圆角卡 + 段头标签)——
阅读方向 RTL/LTR 走 mr_reader_set_direction;启动 PIN 开关与修改(开启/修改均两遍
MEOW_IME_NUM 确认);版本与主题状态行(跟随系统 + 当前明暗,随 app.c 主题 listener
整帧重绘)。控件一律在 on_enter 建(栈顶=ACT_SETTINGS),避免 §6.1 owner 绑到
ACT_LIBRARY 的跨页命中泄漏。
ui/lock(ACT_LOCK):lock_view_should_show() 读 logic/pin;启用时启动先入锁屏,验证前
不绘制任何书库内容;「输入 PIN」按钮弹系统数字键盘(MEOW_IME_NUM),错 PIN 就地提示
重试(无惩罚性锁定),正确则 page_pop 回书库;on_event 消费 SWIPE——左缘返回不能绕过
PIN 或退出 App(meow_view_dispatch_event 先于 app.c 的 on_swipe)。
logic/reader(评审 P1 修复,最小持久化):mr_reader_set_direction 成功切换后 best-effort
写 meow_config("reader.dir","rtl"/"ltr"),写失败只打 WARN 不阻断本次切换;
mr_reader_get_direction 首次调用惰性读一次配置恢复(无配置/非法值 → 默认 RTL),之后
纯内存快路径。宿主测试经 -DMR_READER_TEST_HOOKS 的 mr_reader_test_reset() 模拟重启。
测试/构建:test_pin(85 项,PIN 全分支,含单键半途落盘失败);test_settings_reader_persist
(19 项:默认/落盘/重启恢复/坏值/非法值/只读一次/写失败只告警);可选宿主集成
test_settings_lock_host(真实 SDK view/activity/widget + IME 注入,端到端验证错/对 PIN
解锁与设置页两遍开启;SDK 无源码时 SKIP);run_tests.sh 保持 46/46 并纳入上述测试
(reader.c 的配置/日志依赖由 test/settings_reader_stub.c 存根),build_tuku.sh 加入
logic/pin.c。交叉构建静态 MIPS ELF(zig-mipsel)零警告。
安全口径:PIN 防随手翻看,不是安全边界——TF 卡可拔出直读、配置可改;存储不使用加密库,
锁屏与设置页文案均如实标注该局限。
210 lines
7.8 KiB
C
210 lines
7.8 KiB
C
/* ui/lock/lock.c — 锁屏页(ACT_LOCK):APP 启动 PIN。
|
||
*
|
||
* 流程(用户原话「允许用户在APP设置中设置APP启动 PIN」):
|
||
* - lock_view_should_show() = logic/pin 的 mr_pin_enabled();app.c 据此在启动时先 push
|
||
* 本页,验证通过前书库不进入/不绘制(view 注册表只画已进入的页,widget 还按 owner 过滤);
|
||
* - 本页 on_enter 建「输入 PIN」按钮(owner=ACT_LOCK),点按弹系统数字键盘(MEOW_IME_NUM),
|
||
* 校验通过 → page_pop 回书库;错误 → 就地提示重试(不做惩罚性锁定/计数);
|
||
* - 左缘返回**必须**由本页 on_event 消费 SWIPE(meow_view_dispatch_event 先于 app.c 的
|
||
* on_swipe;否则 app.c 对非 library 页无条件 page_pop,PIN 会被绕过);
|
||
* - PIN 未启用时 app.c 直接 push 书库,本页不会被进入。
|
||
*
|
||
* 安全口径(诚实标注):PIN 防的是随手翻看,不是安全边界——TF 卡可拔出直读,配置目录
|
||
* 可改。存储为加盐迭代哈希(logic/pin.c),不引入加密库;锁屏文案也如实告知用户。
|
||
*
|
||
* 控件归属(§6.1,M3 桩注释①):on_create 时 activity 栈为空,控件一律在 on_enter 建。
|
||
*/
|
||
#include "tuku_internal.h"
|
||
#include "logic/pin.h"
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
static mr_page_ctrl g_ctrl;
|
||
|
||
static meow_widget *g_btn;
|
||
|
||
static char g_msg[64];
|
||
static int g_msg_err;
|
||
static int g_waiting; /* 1=数字键盘打开中(上部提示改为输入说明) */
|
||
static char g_ime_pin[MR_PIN_MAX_LEN + 12];
|
||
|
||
/* meow_view on_event 的事件码:公共伞头未导出(sdk/meow_ui_internal.h:
|
||
* MEOW_EV_TAP=2 / MEOW_EV_SWIPE=3,值稳定),此处按契约镜像。 */
|
||
#define MR_VEV_TAP 2
|
||
#define MR_VEV_SWIPE 3
|
||
|
||
int lock_view_should_show(void)
|
||
{
|
||
return mr_pin_enabled();
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* PIN 输入(系统数字键盘) */
|
||
/* ============================================================ */
|
||
static void ime_pin_change(const char *text, void *ctx)
|
||
{
|
||
(void)ctx;
|
||
snprintf(g_ime_pin, sizeof g_ime_pin, "%s", text ? text : "");
|
||
}
|
||
|
||
/* 阻塞数字键盘;返回 1=收集到非空文本(关闭键盘即结束/校验)。
|
||
* SDK 现状:键盘「确定」只置内部 submit 标记不关闭,关闭路径(取消/点背景)又把标记清 0,
|
||
* 故 meow_ui_text_input 的返回值不可作“确认”判据;文本经 on_change 实时收集,空=取消。 */
|
||
static int lock_read_pin(char *out, int outsz)
|
||
{
|
||
meow_ime_ctx c;
|
||
memset(&c, 0, sizeof c);
|
||
c.mode = MEOW_IME_NUM;
|
||
c.maxlen = (int)sizeof g_ime_pin;
|
||
c.on_change = ime_pin_change;
|
||
c.ctx = NULL;
|
||
|
||
g_ime_pin[0] = '\0';
|
||
g_msg[0] = '\0';
|
||
g_msg_err = 0;
|
||
g_waiting = 1; /* 锁屏上部提示改为输入说明 */
|
||
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height()); /* 键盘区背景快照取当前帧 */
|
||
(void)meow_ui_text_input(&c);
|
||
g_waiting = 0;
|
||
|
||
snprintf(out, (size_t)outsz, "%s", g_ime_pin);
|
||
return out[0] != '\0';
|
||
}
|
||
|
||
static void lock_attempt(void)
|
||
{
|
||
char pin[32] = "";
|
||
if (!lock_read_pin(pin, (int)sizeof pin)) {
|
||
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
|
||
return; /* 关闭键盘未输入:留在锁屏 */
|
||
}
|
||
|
||
if (!mr_pin_verify(pin)) {
|
||
snprintf(g_msg, sizeof g_msg, "PIN 错误,请重试");
|
||
g_msg_err = 1;
|
||
meow_ui_invalidate(0, 0, meow_ui_width(), meow_ui_height());
|
||
return;
|
||
}
|
||
|
||
g_msg[0] = '\0';
|
||
g_msg_err = 0;
|
||
meow_view_page_pop(); /* 通过 → 回书库(ACT_LIBRARY on_enter) */
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 布局/绘制 */
|
||
/* ============================================================ */
|
||
static void lock_draw(meow_view *v, const meow_theme *th, void *ctx)
|
||
{
|
||
(void)v; (void)ctx;
|
||
int w = meow_ui_width();
|
||
int h = meow_ui_height();
|
||
int sb = meow_ui_statusbar_height();
|
||
int cy = sb + (h - sb) / 2;
|
||
|
||
/* 自绘整页底色:验证前不露任何书库内容(本页是顶层唯一 entered 页)。 */
|
||
meow_ui_fill_rect(0, sb, w, h - sb, th->bg);
|
||
|
||
meow_ui_text_center(w / 2, cy - 150, "图库已锁定", th->font_title, th->onSurface);
|
||
if (g_waiting) {
|
||
meow_ui_text_center(w / 2, cy - 104, "输入 4-8 位数字后关闭键盘", th->font_body,
|
||
th->onSurface);
|
||
meow_ui_text_center(w / 2, cy - 76, "空输入 = 取消,回到锁屏", th->font_body,
|
||
th->onSurfaceVariant);
|
||
} else {
|
||
meow_ui_text_center(w / 2, cy - 104, "输入启动 PIN 继续", th->font_body,
|
||
th->onSurfaceVariant);
|
||
meow_ui_text_center(w / 2, cy - 76, "4-8 位数字 · 系统数字键盘", th->font_body,
|
||
th->onSurfaceVariant);
|
||
}
|
||
|
||
if (g_msg[0]) {
|
||
meow_ui_text_center(w / 2, cy - 26, g_msg, th->font_body,
|
||
g_msg_err ? th->error : th->primary);
|
||
}
|
||
|
||
/* 安全口径(如实告知):PIN 不是安全边界。 */
|
||
meow_ui_text_center(w / 2, h - 86, "PIN 仅用于防止随手查看,", th->font_body,
|
||
th->onSurfaceVariant);
|
||
meow_ui_text_center(w / 2, h - 58, "TF 卡可被直接读取,不构成安全边界。", th->font_body,
|
||
th->onSurfaceVariant);
|
||
}
|
||
|
||
static void lock_place(meow_view *v)
|
||
{
|
||
int w = meow_ui_width();
|
||
int h = meow_ui_height();
|
||
int sb = meow_ui_statusbar_height();
|
||
int cy = sb + (h - sb) / 2;
|
||
|
||
v->x = 0; v->y = 0; v->w = w; v->h = h;
|
||
|
||
int bw = 220, bh = 52;
|
||
if (g_btn) meow_widget_set_rect(g_btn, (w - bw) / 2, cy + 20, bw, bh);
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* 事件 */
|
||
/* ============================================================ */
|
||
static int lock_on_event(meow_view *v, int ev, int a, int b, void *ctx)
|
||
{
|
||
(void)v; (void)ctx;
|
||
|
||
/* 左缘返回(以及任何滑动)一律吞掉:锁屏不允许退出 App,也不允许绕过 PIN。 */
|
||
if (ev == MR_VEV_SWIPE) return 1;
|
||
|
||
if (ev == MR_VEV_TAP) {
|
||
int x = a, y = b;
|
||
if (meow_widget_hit(g_btn, x, y)) lock_attempt();
|
||
return 1; /* 锁屏页消费点击,不向 App 其它页透传 */
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
/* ============================================================ */
|
||
/* view 生命周期 */
|
||
/* ============================================================ */
|
||
static void lock_page_enter(meow_view *v, void *ctx)
|
||
{
|
||
((mr_page_ctrl *)ctx)->active = 1;
|
||
meow_ui_set_fullscreen(0);
|
||
meow_ui_set_page_bg(meow_theme_get()->bg);
|
||
|
||
g_msg[0] = '\0';
|
||
g_msg_err = 0;
|
||
|
||
if (!g_btn) {
|
||
/* page_push 已先 meow_ui_activity_push(ACT_LOCK) → 控件 owner 正确。 */
|
||
g_btn = meow_btn_create_variant(0, 0, 220, 52, "输入 PIN", MEOW_BTN_FILLED);
|
||
if (g_btn) meow_view_add_widget(v, g_btn);
|
||
}
|
||
lock_place(v);
|
||
mr_nav_report();
|
||
meow_ui_paint_full();
|
||
}
|
||
|
||
static void lock_page_leave(meow_view *v, void *ctx)
|
||
{
|
||
(void)v;
|
||
((mr_page_ctrl *)ctx)->active = 0;
|
||
}
|
||
|
||
static void lock_page_destroy(meow_view *v, void *ctx)
|
||
{
|
||
(void)v; (void)ctx;
|
||
g_btn = NULL; /* 控件随 meow_view_free 释放(v->widgets) */
|
||
}
|
||
|
||
meow_view *lock_view_create(void)
|
||
{
|
||
static const meow_view_hooks h = {
|
||
.on_enter = lock_page_enter,
|
||
.on_leave = lock_page_leave,
|
||
.on_destroy = lock_page_destroy,
|
||
.on_draw = lock_draw,
|
||
.on_event = lock_on_event,
|
||
};
|
||
return meow_view_create(ACT_LOCK, MEOW_VIEW_ACTIVITY, &h, &g_ctrl);
|
||
}
|