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 卡可拔出直读、配置可改;存储不使用加密库,
锁屏与设置页文案均如实标注该局限。
212 lines
7.0 KiB
C
212 lines
7.0 KiB
C
/* 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';
|
||
}
|