Files
MeowOS-SDK/tuku/logic/pin.c
T
AskaEth cca3906c80 图库: 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 卡可拔出直读、配置可改;存储不使用加密库,
锁屏与设置页文案均如实标注该局限。
2026-11-04 21:25:17 +08:00

212 lines
7.0 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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';
}