Files
MeowOS-SDK/tuku/test/test_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

321 lines
12 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.
/* test_pin.c — 启动 PIN(logic/pin)宿主测试:纯 C,不链 SDK。
*
* 用内存 KV 存根顶替 meow_config_get/set/int/remove(与 SDK 语义一致:
* 未设 → MEOW_CFG_ERR_NOENT;写失败可由 g_fail_write 注入)。
* 覆盖:格式校验边界(4/8 位合法;3/9 位、空串、NULL、符号、全角数字非法)、
* 设置/校验、错 PIN、重设换盐、change 全分支、disable、配置残缺 fail-closed、
* 落盘失败、落盘布局(pin.enabled/hash/salt)。
*
* 编译(在 tuku/ 下):
* gcc -Wall -Wextra -O1 -I. -I"$SDK" test/test_pin.c logic/pin.c -o test/test_pin
* 或直接 test/run_tests.sh。
*/
#include "logic/pin.h"
#include "include/meow_ui_config.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define KV_MAX 8
static struct { char key[64]; char val[128]; int used; } g_kv[KV_MAX];
static int g_fail_write; /* 1=模拟全部落盘失败 */
static const char *g_fail_key; /* 非 NULL=只让该键写失败(模拟半途失败) */
static int kv_find(const char *key)
{
for (int i = 0; i < KV_MAX; i++)
if (g_kv[i].used && strcmp(g_kv[i].key, key) == 0) return i;
return -1;
}
static void kv_clear(void)
{
memset(g_kv, 0, sizeof g_kv);
g_fail_write = 0;
g_fail_key = NULL;
}
int meow_config_set(const char *key, const char *val)
{
if (!key || !key[0]) return MEOW_CFG_ERR_INVAL;
if (g_fail_write || (g_fail_key && strcmp(g_fail_key, key) == 0)) return MEOW_CFG_ERR_IO;
int i = kv_find(key);
if (i < 0) {
for (i = 0; i < KV_MAX; i++) if (!g_kv[i].used) break;
if (i >= KV_MAX) return MEOW_CFG_ERR_IO;
g_kv[i].used = 1;
snprintf(g_kv[i].key, sizeof g_kv[i].key, "%s", key);
}
snprintf(g_kv[i].val, sizeof g_kv[i].val, "%s", val ? val : "");
return MEOW_CFG_OK;
}
int meow_config_get(const char *key, char *out, int outsz)
{
if (!out || outsz <= 0) return MEOW_CFG_ERR_INVAL;
out[0] = '\0';
int i = kv_find(key);
if (i < 0) return MEOW_CFG_ERR_NOENT;
snprintf(out, (size_t)outsz, "%s", g_kv[i].val);
return MEOW_CFG_OK;
}
int meow_config_int(const char *key, int def)
{
char buf[32];
char *end = NULL;
long v;
if (meow_config_get(key, buf, sizeof buf) != MEOW_CFG_OK) return def;
v = strtol(buf, &end, 10);
if (end == buf) return def;
return (int)v;
}
int meow_config_bool(const char *key, int def)
{
return meow_config_int(key, def) != 0;
}
void meow_config_remove(const char *key)
{
int i = kv_find(key);
if (i >= 0) g_kv[i].used = 0;
}
const char *meow_config_dir(void) { return "/tmp/meowcfg_test"; }
static int g_pass, g_fail;
#define CHECK(cond, ...) do { \
if (cond) { g_pass++; } \
else { g_fail++; printf("FAIL %s:%d ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } \
} while (0)
static int is_hex16(const char *s)
{
if (!s) return 0;
if (strlen(s) != 16) return 0;
for (int i = 0; i < 16; i++)
if (!((s[i] >= '0' && s[i] <= '9') || (s[i] >= 'a' && s[i] <= 'f'))) return 0;
return 1;
}
static void test_format(void)
{
CHECK(mr_pin_valid(NULL) == 0, "NULL invalid");
CHECK(mr_pin_valid("") == 0, "empty invalid");
CHECK(mr_pin_valid("123") == 0, "3 digits invalid");
CHECK(mr_pin_valid("1234") == 1, "4 digits valid (min)");
CHECK(mr_pin_valid("12345") == 1, "5 digits valid");
CHECK(mr_pin_valid("12345678") == 1, "8 digits valid (max)");
CHECK(mr_pin_valid("123456789") == 0, "9 digits invalid");
CHECK(mr_pin_valid("12a4") == 0, "letter invalid");
CHECK(mr_pin_valid("12 4") == 0, "space invalid");
CHECK(mr_pin_valid(" 1234") == 0, "leading space invalid");
CHECK(mr_pin_valid("1234 ") == 0, "trailing space invalid");
CHECK(mr_pin_valid("-123") == 0, "sign invalid");
/* 全角数字(UTF-8 EF BC 9x)不是 ASCII 数字 */
CHECK(mr_pin_valid("\xEF\xBC\x91\xEF\xBC\x92\xEF\xBC\x93\xEF\xBC\x94") == 0,
"fullwidth digits invalid");
}
static void test_default_state(void)
{
kv_clear();
CHECK(mr_pin_enabled() == 0, "default disabled");
CHECK(mr_pin_verify("1234") == 0, "verify on disabled = 0");
CHECK(mr_pin_set("123") == MR_PIN_ERR_FORMAT, "set short -> format error");
CHECK(mr_pin_enabled() == 0, "failed set keeps disabled");
}
static void test_set_verify(void)
{
kv_clear();
CHECK(mr_pin_set("1234") == MR_PIN_OK, "set 1234 ok");
CHECK(mr_pin_enabled() == 1, "enabled after set");
CHECK(mr_pin_verify("1234") == 1, "verify correct");
CHECK(mr_pin_verify("4321") == 0, "verify wrong");
CHECK(mr_pin_verify("123") == 0, "verify short");
CHECK(mr_pin_verify("12345") == 0, "verify longer");
CHECK(mr_pin_verify("") == 0, "verify empty");
CHECK(mr_pin_verify(NULL) == 0, "verify NULL");
CHECK(mr_pin_verify("12a4") == 0, "verify non-digit");
char salt[32] = "", hash[32] = "";
CHECK(mr_pin_get_salt(salt, sizeof salt) == 1, "salt present");
CHECK(mr_pin_get_hash(hash, sizeof hash) == 1, "hash present");
CHECK(is_hex16(salt), "salt is 16 hex (got %s)", salt);
CHECK(is_hex16(hash), "hash is 16 hex (got %s)", hash);
CHECK(strcmp(hash, "1234") != 0, "hash is not the raw pin");
/* 落盘布局:enabled="1"(明文 PIN 不落盘:配置里只有 16 位 hex 摘要与盐) */
char buf[64] = "";
CHECK(meow_config_get("pin.enabled", buf, sizeof buf) == MEOW_CFG_OK && strcmp(buf, "1") == 0,
"pin.enabled=1 on disk");
}
static void test_reset_new_salt(void)
{
kv_clear();
CHECK(mr_pin_set("1234") == MR_PIN_OK, "set first");
char salt1[32] = "", hash1[32] = "";
mr_pin_get_salt(salt1, sizeof salt1);
mr_pin_get_hash(hash1, sizeof hash1);
CHECK(mr_pin_set("5678") == MR_PIN_OK, "set second (overwrite)");
char salt2[32] = "", hash2[32] = "";
mr_pin_get_salt(salt2, sizeof salt2);
mr_pin_get_hash(hash2, sizeof hash2);
CHECK(strcmp(salt1, salt2) != 0, "new salt on each set");
CHECK(strcmp(hash1, hash2) != 0, "new hash on each set");
CHECK(mr_pin_verify("5678") == 1, "new pin verifies");
CHECK(mr_pin_verify("1234") == 0, "old pin rejected after overwrite");
/* 同一 PIN 重设:盐不同 → 摘要也不同(防跨设备彩虹表/同值比对)。 */
CHECK(mr_pin_set("5678") == MR_PIN_OK, "re-set same pin");
char salt3[32] = "", hash3[32] = "";
mr_pin_get_salt(salt3, sizeof salt3);
mr_pin_get_hash(hash3, sizeof hash3);
CHECK(strcmp(salt2, salt3) != 0, "salt differs for same pin");
CHECK(strcmp(hash2, hash3) != 0, "hash differs for same pin");
CHECK(mr_pin_verify("5678") == 1, "same pin still verifies");
}
static void test_change(void)
{
kv_clear();
CHECK(mr_pin_change("1234", "5678") == MR_PIN_ERR_STATE, "change while disabled -> state");
CHECK(mr_pin_set("1234") == MR_PIN_OK, "setup 1234");
CHECK(mr_pin_change("9999", "5678") == MR_PIN_ERR_WRONG, "change wrong old -> wrong");
CHECK(mr_pin_verify("1234") == 1, "old still valid after wrong-change");
CHECK(mr_pin_verify("5678") == 0, "new not applied after wrong-change");
CHECK(mr_pin_change("1234", "12") == MR_PIN_ERR_FORMAT, "change to short new -> format");
CHECK(mr_pin_verify("1234") == 1, "old still valid after bad-new");
CHECK(mr_pin_change("1234", "12345678") == MR_PIN_OK, "change ok (8 digits)");
CHECK(mr_pin_verify("12345678") == 1, "new valid after change");
CHECK(mr_pin_verify("1234") == 0, "old invalid after change");
CHECK(mr_pin_enabled() == 1, "still enabled after change");
CHECK(mr_pin_change(NULL, "0000") == MR_PIN_ERR_WRONG, "change NULL old -> wrong");
CHECK(mr_pin_change("12345678", NULL) == MR_PIN_ERR_FORMAT, "change NULL new -> format");
}
static void test_disable(void)
{
kv_clear();
CHECK(mr_pin_disable() == MR_PIN_OK, "disable while disabled is idempotent");
CHECK(mr_pin_set("1234") == MR_PIN_OK, "setup for disable");
CHECK(mr_pin_disable() == MR_PIN_OK, "disable ok");
CHECK(mr_pin_enabled() == 0, "disabled after disable");
CHECK(mr_pin_verify("1234") == 0, "verify fails after disable");
char buf[32] = "";
CHECK(meow_config_get("pin.hash", buf, sizeof buf) == MEOW_CFG_ERR_NOENT, "hash key removed");
CHECK(meow_config_get("pin.salt", buf, sizeof buf) == MEOW_CFG_ERR_NOENT, "salt key removed");
CHECK(meow_config_get("pin.enabled", buf, sizeof buf) == MEOW_CFG_OK && strcmp(buf, "0") == 0,
"enabled=0 on disk");
/* 禁用后可重新开启(新盐)。 */
CHECK(mr_pin_set("8765") == MR_PIN_OK, "re-enable after disable");
CHECK(mr_pin_verify("8765") == 1, "re-enabled pin verifies");
}
static void test_corrupt_state(void)
{
/* enabled=1 但缺 hash → fail-closed */
kv_clear();
meow_config_set("pin.enabled", "1");
meow_config_set("pin.salt", "0011223344556677");
CHECK(mr_pin_enabled() == 0, "enabled without hash -> fail closed");
CHECK(mr_pin_verify("1234") == 0, "verify fail closed");
/* hash 长度/字符非法 */
kv_clear();
meow_config_set("pin.enabled", "1");
meow_config_set("pin.salt", "0011223344556677");
meow_config_set("pin.hash", "zz");
CHECK(mr_pin_enabled() == 0, "garbage hash -> fail closed");
/* salt 非法 */
kv_clear();
meow_config_set("pin.enabled", "1");
meow_config_set("pin.salt", "");
meow_config_set("pin.hash", "0011223344556677");
CHECK(mr_pin_enabled() == 0, "empty salt -> fail closed");
/* change 在残缺状态 → state 错误(不误判为已启用) */
CHECK(mr_pin_change("1234", "5678") == MR_PIN_ERR_STATE, "change on corrupt -> state");
}
static void test_store_failure(void)
{
kv_clear();
g_fail_write = 1;
CHECK(mr_pin_set("1234") == MR_PIN_ERR_STORE, "set with failing store -> store error");
CHECK(mr_pin_enabled() == 0, "still disabled after store failure");
g_fail_write = 0;
/* 写盘失败时不得把状态改成「已启用」——set 先写盐/摘要、最后写 enabled。 */
CHECK(mr_pin_set("1234") == MR_PIN_OK, "set ok after store recovers");
g_fail_write = 1;
CHECK(mr_pin_disable() == MR_PIN_ERR_STORE, "disable with failing store -> store error");
g_fail_write = 0;
}
static void test_partial_write(void)
{
/* 摘要写失败:把盐还原成旧值,已启用的旧 PIN 仍可校验(不会新旧都验不过)。 */
kv_clear();
CHECK(mr_pin_set("1234") == MR_PIN_OK, "setup 1234");
g_fail_key = "pin.hash";
CHECK(mr_pin_set("5678") == MR_PIN_ERR_STORE, "hash write failure -> store error");
CHECK(mr_pin_enabled() == 1, "still enabled after partial write");
CHECK(mr_pin_verify("1234") == 1, "old PIN still valid after partial write");
CHECK(mr_pin_verify("5678") == 0, "new PIN not applied after partial write");
g_fail_key = NULL;
/* 重设时 enabled 已是 "1":该键写失败不影响一致性 → 按成功返回。 */
g_fail_key = "pin.enabled";
CHECK(mr_pin_set("4321") == MR_PIN_OK, "enabled-already-1 write failure is consistent");
CHECK(mr_pin_verify("4321") == 1, "new PIN active");
g_fail_key = NULL;
/* 首次开启时 enabled 写失败 → 未启用(fail-closed)。 */
kv_clear();
g_fail_key = "pin.enabled";
CHECK(mr_pin_set("1111") == MR_PIN_ERR_STORE, "first enable with enabled-write failure");
CHECK(mr_pin_enabled() == 0, "not enabled after enabled-write failure");
g_fail_key = NULL;
}
static void test_verify_does_not_mutate(void)
{
kv_clear();
CHECK(mr_pin_set("2468") == MR_PIN_OK, "setup");
char salt1[32] = "", salt2[32] = "";
mr_pin_get_salt(salt1, sizeof salt1);
(void)mr_pin_verify("0000");
(void)mr_pin_verify("2468");
mr_pin_get_salt(salt2, sizeof salt2);
CHECK(strcmp(salt1, salt2) == 0, "verify does not change salt");
}
int main(void)
{
test_format();
test_default_state();
test_set_verify();
test_reset_new_salt();
test_change();
test_disable();
test_corrupt_state();
test_store_failure();
test_partial_write();
test_verify_does_not_mutate();
printf("%s: %d passed, %d failed\n", g_fail ? "FAIL" : "PASS", g_pass, g_fail);
return g_fail ? 1 : 0;
}