图库: M5 设置页(阅读方向/PIN/主题状态)与启动 PIN 锁屏;阅读方向持久化(评审 P1)

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 卡可拔出直读、配置可改;存储不使用加密库,
锁屏与设置页文案均如实标注该局限。
This commit is contained in:
AskaEth committed 2026-11-04 21:25:17 +08:00
1 parent 47325f2b81
commit cca3906c80
12 files changed
+1749 -60

No files matched your search

+211
View File
@@ -0,0 +1,211 @@
/* logic/pin.c — APP 启动 PIN:加盐迭代哈希 + meow_config 持久化(纯 C)。
*
* 设计取舍(见 pin.h 头注释):PIN 防随手翻看,不是安全边界。本文件不引入加密库,
* 只做「每次设置新随机盐 + FNV-1a 迭代 1000 轮」的轻量哈希,避免明文 PIN 直接躺在
* 配置里。配置落 TF 数据目录,卡被拔走即可读/改——这是产品口径接受的局限。
*
* 依赖面:只 include include/meow_ui_config.h(get/set/remove/int 声明),不 include
* meow_ui.h、不碰 UI,宿主 gcc 可直接编译 + 测试存根替换配置层。
*/
#include "pin.h"
#include "include/meow_ui_config.h"
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#define PIN_KEY_ENABLED "pin.enabled"
#define PIN_KEY_HASH "pin.hash"
#define PIN_KEY_SALT "pin.salt"
#define PIN_HEX_LEN 16 /* 64-bit 摘要/盐的 hex 字符数 */
#define PIN_HEX_BUF 17 /* 16 + NUL */
#define PIN_ROUNDS 1000 /* 迭代轮数(防直接反查,不防暴力枚举) */
#define PIN_SALT_BYTES 8 /* 8 字节盐 → 16 位 hex */
/* ---------- 哈希 ---------- */
/* FNV-1a(64-bit)逐段吸收,返回新状态。 */
static uint64_t fnv1a_update(uint64_t h, const void *data, size_t n)
{
const unsigned char *p = (const unsigned char *)data;
for (size_t i = 0; i < n; i++) {
h ^= (uint64_t)p[i];
h *= 1099511628211ULL;
}
return h;
}
/* pin → 16 位 hex 摘要:FNV-1a(salt, pin) 起手,再做 PIN_ROUNDS 轮
* 「上一轮摘要(大端 8 字节)+ 盐」的 FNV-1a。同一 PIN 每次设置因盐不同而摘要不同。 */
static void pin_digest(const char *pin, const char *salt, char out[PIN_HEX_BUF])
{
uint64_t h = 1469598103934665603ULL; /* FNV-1a 64-bit offset basis */
h = fnv1a_update(h, salt, strlen(salt));
h = fnv1a_update(h, pin, strlen(pin));
for (int r = 0; r < PIN_ROUNDS; r++) {
unsigned char be[8];
for (int i = 0; i < 8; i++)
be[i] = (unsigned char)((h >> (8 * (7 - i))) & 0xFF);
h = fnv1a_update(h, be, sizeof be);
h = fnv1a_update(h, salt, strlen(salt));
}
snprintf(out, PIN_HEX_BUF, "%016llx", (unsigned long long)h);
}
/* 生成 16 位 hex 随机盐:优先 /dev/urandom;无设备时用时间/pid/地址/计数器做降级熵
* (宿主测试与裁剪系统仍可用——降级盐的强度如实标注:不构成密码学随机)。 */
static void pin_gen_salt(char out[PIN_HEX_BUF])
{
unsigned char b[PIN_SALT_BYTES];
int ok = 0;
FILE *f = fopen("/dev/urandom", "rb");
if (f) {
ok = (fread(b, 1, sizeof b, f) == sizeof b);
fclose(f);
}
if (!ok) {
static uint64_t ctr;
uint64_t x = (uint64_t)time(NULL)
^ ((uint64_t)clock() << 21)
^ ((uint64_t)getpid() << 40)
^ (uint64_t)(uintptr_t)&b
^ (++ctr * 0x9E3779B97F4A7C15ULL);
for (int i = 0; i < PIN_SALT_BYTES; i++)
b[i] = (unsigned char)((x >> (8 * i)) & 0xFF);
}
for (int i = 0; i < PIN_SALT_BYTES; i++)
snprintf(out + i * 2, 3, "%02x", b[i]);
}
/* ---------- 配置层小包装 ---------- */
static int cfg_get(const char *key, char *out, int outsz)
{
return meow_config_get(key, out, outsz) == MEOW_CFG_OK ? 0 : -1;
}
static int cfg_set(const char *key, const char *val)
{
return meow_config_set(key, val) == MEOW_CFG_OK ? 0 : -1;
}
static int is_hex16(const char *s)
{
if (!s) return 0;
for (int i = 0; i < PIN_HEX_LEN; i++) {
char c = s[i];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) return 0;
}
return s[PIN_HEX_LEN] == '\0';
}
/* ---------- 公共 API ---------- */
int mr_pin_valid(const char *pin)
{
if (!pin) return 0;
int n = 0;
while (pin[n]) {
if (pin[n] < '0' || pin[n] > '9') return 0;
n++;
if (n > MR_PIN_MAX_LEN) return 0;
}
return n >= MR_PIN_MIN_LEN;
}
int mr_pin_enabled(void)
{
char hash[PIN_HEX_BUF + 8];
char salt[PIN_HEX_BUF + 8];
if (meow_config_int(PIN_KEY_ENABLED, 0) != 1) return 0;
if (cfg_get(PIN_KEY_HASH, hash, (int)sizeof hash) != 0) return 0;
if (cfg_get(PIN_KEY_SALT, salt, (int)sizeof salt) != 0) return 0;
return is_hex16(hash) && is_hex16(salt); /* 配置残缺 → fail-closed */
}
int mr_pin_set(const char *pin)
{
char salt[PIN_HEX_BUF];
char hash[PIN_HEX_BUF];
char old_salt[PIN_HEX_BUF + 8];
int had_old_salt;
if (!mr_pin_valid(pin)) return MR_PIN_ERR_FORMAT;
pin_gen_salt(salt);
pin_digest(pin, salt, hash);
had_old_salt = (cfg_get(PIN_KEY_SALT, old_salt, (int)sizeof old_salt) == 0);
/* 先写盐与摘要、最后打开 enabled:中途失败只会留下「未启用」,不会出现
* enabled=1 但缺少盐/哈希的半残状态。摘要写失败时把盐还原成旧值,使已启用的旧
* PIN 仍可校验(否则「新盐+旧哈希」会让新旧 PIN 都验不过)。 */
if (cfg_set(PIN_KEY_SALT, salt) != 0) return MR_PIN_ERR_STORE;
if (cfg_set(PIN_KEY_HASH, hash) != 0) {
if (had_old_salt) cfg_set(PIN_KEY_SALT, old_salt);
else meow_config_remove(PIN_KEY_SALT);
return MR_PIN_ERR_STORE;
}
if (cfg_set(PIN_KEY_ENABLED, "1") != 0) {
/* 重设时 enabled 本来就是 "1":新盐/哈希已一致生效,按成功返回;
* 首次开启时 enabled 仍为 0 → 如实报落盘失败。 */
return meow_config_int(PIN_KEY_ENABLED, 0) == 1 ? MR_PIN_OK : MR_PIN_ERR_STORE;
}
return MR_PIN_OK;
}
int mr_pin_verify(const char *pin)
{
char hash[PIN_HEX_BUF + 8];
char salt[PIN_HEX_BUF + 8];
char calc[PIN_HEX_BUF];
if (!mr_pin_valid(pin)) return 0;
if (!mr_pin_enabled()) return 0;
if (cfg_get(PIN_KEY_HASH, hash, (int)sizeof hash) != 0) return 0;
if (cfg_get(PIN_KEY_SALT, salt, (int)sizeof salt) != 0) return 0;
pin_digest(pin, salt, calc);
unsigned diff = 0;
for (int i = 0; i < PIN_HEX_LEN; i++)
diff |= (unsigned)((unsigned char)hash[i] ^ (unsigned char)calc[i]);
diff |= (unsigned)(hash[PIN_HEX_LEN] != '\0');
return diff == 0;
}
int mr_pin_change(const char *old_pin, const char *new_pin)
{
if (!mr_pin_enabled()) return MR_PIN_ERR_STATE;
if (!mr_pin_valid(new_pin)) return MR_PIN_ERR_FORMAT;
if (!mr_pin_verify(old_pin)) return MR_PIN_ERR_WRONG;
return mr_pin_set(new_pin);
}
int mr_pin_disable(void)
{
meow_config_remove(PIN_KEY_HASH);
meow_config_remove(PIN_KEY_SALT);
if (cfg_set(PIN_KEY_ENABLED, "0") != 0) return MR_PIN_ERR_STORE;
return MR_PIN_OK;
}
int mr_pin_get_salt(char *out, int outsz)
{
if (!out || outsz <= 0) return 0;
out[0] = '\0';
if (cfg_get(PIN_KEY_SALT, out, outsz) != 0) return 0;
return out[0] != '\0';
}
int mr_pin_get_hash(char *out, int outsz)
{
if (!out || outsz <= 0) return 0;
out[0] = '\0';
if (cfg_get(PIN_KEY_HASH, out, outsz) != 0) return 0;
return out[0] != '\0';
}
+62
View File
@@ -0,0 +1,62 @@
/* logic/pin.h — APP 启动 PIN(纯 C;宿主可裸测)。
*
* 定位(诚实标注):PIN 只挡「随手翻看」——TF 卡可拔出直读,数据目录明文可改,
* 故不引入加密库、不宣称安全边界。存储口径:加盐迭代哈希(FNV-1a ×1000 轮,
* 每次设置新随机盐,hex 输出),落在 meow_config_*(App 自有 KV → TF MeowData)。
* 盐/哈希只做「不知道 PIN 无法直接重算」的轻量门槛,防不了离线暴力或改配置。
*
* 配置键(meow_config_*,值均为字符串):
* pin.enabled = "1"/"0"
* pin.hash = 16 位 hex(64-bit 摘要)
* pin.salt = 16 位 hex(每次设置重新生成)
* enabled=1 但 hash/salt 缺失或格式不合法时一律按「未启用」处理(fail-closed)。
*
* 本模块只依赖 include/meow_ui_config.h 的三个声明(get/set/remove/int),不碰 UI;
* 宿主测试链 test_pin.c 内的 meow_config_* 存根即可,无需 SDK/设备。
*/
#ifndef MR_PIN_H
#define MR_PIN_H
#ifdef __cplusplus
extern "C" {
#endif
#define MR_PIN_MIN_LEN 4 /* 允许的最短位数 */
#define MR_PIN_MAX_LEN 8 /* 允许的最长位数 */
/* 返回码(set/change/disable)。verify 用 1/0 表示对/错,不返回错误码。 */
#define MR_PIN_OK 0 /* 成功 */
#define MR_PIN_ERR_FORMAT (-1) /* PIN 非法:非 4-8 位纯数字(NULL/空/符号/超长) */
#define MR_PIN_ERR_STATE (-2) /* 状态不满足:change 时尚未启用 */
#define MR_PIN_ERR_WRONG (-3) /* change 时原 PIN 校验失败 */
#define MR_PIN_ERR_STORE (-4) /* meow_config_* 落盘失败(无数据目录/只读/IO) */
/* 4-8 位纯 ASCII 数字('0'-'9')→ 1;其余(含 NULL、空串、符号、全角数字)→ 0。 */
int mr_pin_valid(const char *pin);
/* 当前是否启用:1=已启用且 hash/salt 完整合法;0=未启用或配置损坏(fail-closed)。 */
int mr_pin_enabled(void);
/* 设置/重设 PIN(生成新盐 + 迭代哈希并立即落盘)。成功即生效(下次启动进锁屏)。
* 返回 MR_PIN_OK / MR_PIN_ERR_FORMAT / MR_PIN_ERR_STORE。 */
int mr_pin_set(const char *pin);
/* 校验:1=匹配;0=不匹配/未启用/输入非法(不区分,避免侧信道式提示)。 */
int mr_pin_verify(const char *pin);
/* 修改:先校验 old,再以新盐重设 new。失败时旧 PIN 保持有效(先校验后写盘)。
* 返回 MR_PIN_OK / MR_PIN_ERR_STATE / MR_PIN_ERR_FORMAT / MR_PIN_ERR_WRONG / MR_PIN_ERR_STORE。 */
int mr_pin_change(const char *old_pin, const char *new_pin);
/* 关闭 PIN:删 hash/salt 并把 enabled 置 0(幂等;调用方已通过锁屏验证时无需再输 PIN)。
* 返回 MR_PIN_OK / MR_PIN_ERR_STORE。 */
int mr_pin_disable(void);
/* 诊断/测试用:读回当前 hex 盐/哈希。返回 1=读成功,0=未设。 */
int mr_pin_get_salt(char *out, int outsz);
int mr_pin_get_hash(char *out, int outsz);
#ifdef __cplusplus
}
#endif
#endif /* MR_PIN_H */
+49 -2
View File
@@ -1,14 +1,61 @@
/* logic/reader.c — 阅读方向状态(默认 RTL;M4 的阅读器直接复用)。 */
/* logic/reader.c — 阅读方向状态(默认 RTL;M4/M6 的阅读器直接复用)。
*
* M5 评审 P1 修复:方向设置要「下次启动生效」。最小持久化(不新增任何方向状态):
* - mr_reader_set_direction 成功切换后 best-effort 写 meow_config("reader.dir","rtl"/"ltr");
* 写失败只打 WARN,不阻断切换(本次运行仍生效);
* - mr_reader_get_direction 首次调用惰性读一次配置恢复;无配置/值非法 → 默认 RTL;
* 之后走纯内存快路径(不再读配置)。
* 依赖面:只 include 配置/日志两个模块头(不含 UI);宿主测试用
* test/settings_reader_stub.c 顶替 meow_config_* 与 meow_log。
*/
#include "reader.h"
#include "include/meow_ui_config.h"
#include "include/meow_ui_log.h"
#include <string.h>
#define MR_DIR_CFG_KEY "reader.dir"
#define MR_DIR_CFG_LTR "ltr"
#define MR_DIR_CFG_RTL "rtl"
static int g_direction = MR_DIR_RTL;
static int g_restored; /* 0=尚未从配置恢复(首次 get/set 时读一次) */
static void reader_restore_once(void)
{
char buf[8];
if (g_restored) return;
g_restored = 1; /* 无配置也只读一次:之后纯内存 */
if (meow_config_get(MR_DIR_CFG_KEY, buf, sizeof buf) != MEOW_CFG_OK) return;
if (strcmp(buf, MR_DIR_CFG_LTR) == 0) g_direction = MR_DIR_LTR;
else if (strcmp(buf, MR_DIR_CFG_RTL) == 0) g_direction = MR_DIR_RTL;
/* 其它值:忽略,保持默认 RTL */
}
int mr_reader_get_direction(void)
{
reader_restore_once();
return g_direction;
}
void mr_reader_set_direction(int dir)
{
if (dir == MR_DIR_RTL || dir == MR_DIR_LTR) g_direction = dir;
if (dir != MR_DIR_RTL && dir != MR_DIR_LTR) return;
reader_restore_once(); /* 先恢复:避免「没 get 过就 set」把配置里的旧方向当默认值 */
if (g_direction == dir) return; /* 无变化不写盘 */
g_direction = dir;
if (meow_config_set(MR_DIR_CFG_KEY, dir == MR_DIR_LTR ? MR_DIR_CFG_LTR : MR_DIR_CFG_RTL)
!= MEOW_CFG_OK)
meow_log(MEOW_LOG_WARN, "reader.dir: save failed (best-effort, kept in memory)");
}
#ifdef MR_READER_TEST_HOOKS
void mr_reader_test_reset(void)
{
g_direction = MR_DIR_RTL;
g_restored = 0;
}
#endif