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