fix(ncm): 会话密钥改用设备私有 .mkey,网卡/MAC 变化不再丢登录态(M9 启动无网根因)

根因(代码级):会话 cookie 加密密钥 = sha256(首选网卡 MAC + 盐),MAC 取自
/sys/class/net 首个非 lo 网卡。设备断网时 Wi-Fi 网卡可能从 /sys/class/net 消失,
或 readdir 顺序变化选到另一块网卡 → 派生密钥改变 → session.enc 解不开 →
ncm_ctx_load_cookie 失败 → cookie 为空 → g_st.logged_in=0 → 启动直接进扫码页。
这与「把网络不可达当登录过期」是同一症状的另一条路径,且发生在登录门之前。

改动:
- 新密钥优先 <data_dir>/.mkey(32B 随机,首次生成后稳定,与网络状态无关);
  .mkey 不可写(只读 FS/无权限)才回退旧 MAC 派生,保持极端环境可启动。
- 旧版 MAC 派生密文仍可解(cookie_decrypt_sel 二次尝试);load_session 命中旧密钥时
  自动重写为 .mkey 密钥(透明迁移,不改调用方)。
- ncm_store.h 同步说明:machine_key 仅作旧密文兼容,新写入不再使用。
This commit is contained in:
AskaEth committed 2026-11-05 11:45:20 +08:00
1 parent 707bbd8a02
commit e0bd503261
2 files changed
+66 -12

No files matched your search

+59 -9
View File
@@ -170,6 +170,28 @@ int ncm_store_machine_key(const char *salt, unsigned char key[16]) {
return 0;
}
/* 设备私有密钥派生:sha256(.mkey_hex + 盐)。.mkey 是首次生成的 32 字节随机数,
* 与网络状态/网卡集合无关。 */
static int key_from_mkey(const char *salt, unsigned char key[16]) {
unsigned char mkey[32], h[32];
if (get_or_create_mkey(mkey) != 0) return -1;
char hex[65], buf[96];
ncm_hex_encode(mkey, 32, hex, 0);
snprintf(buf, sizeof buf, "%s%s", hex, salt ? salt : "");
if (ncm_sha256(buf, strlen(buf), h) != 0) return -1;
memcpy(key, h, 16);
return 0;
}
/* 会话密钥(M9 根因修复):优先设备私有 .mkey,只有它不可用(只读文件系统/无权限)才回退
* 旧版 MAC 派生。旧版密钥依赖「首选网卡 MAC」——设备断网时网卡可能从 /sys/class/net 消失,
* 或 readdir 顺序变化选到另一块网卡,派生结果随之改变 → session.enc 解不开 → App 以为
* 「未登录」直接进扫码页(用户报告的「一断网就退到扫码登录」正是该路径)。 */
static int session_key(const char *salt, unsigned char key[16]) {
if (key_from_mkey(salt, key) == 0) return 0;
return ncm_store_machine_key(salt, key);
}
static int pkcs7_pad_len(size_t n) {
return (int)(16 - (n % 16)); /* 1..16 */
}
@@ -178,7 +200,7 @@ int ncm_store_cookie_encrypt(const char *plain,
unsigned char *out, size_t *out_len, size_t cap) {
if (!plain || !out || !out_len) return NCM_ERR_ARG;
unsigned char key[16];
if (ncm_store_machine_key(SESSION_SALT, key) != 0) return NCM_ERR_CRYPTO;
if (session_key(SESSION_SALT, key) != 0) return NCM_ERR_CRYPTO;
size_t n = strlen(plain);
int pad = pkcs7_pad_len(n);
@@ -212,13 +234,9 @@ int ncm_store_cookie_encrypt(const char *plain,
return NCM_OK;
}
int ncm_store_cookie_decrypt(const unsigned char *in, size_t in_len,
char *out, size_t out_sz) {
if (!in || !out || !out_sz) return NCM_ERR_ARG;
if (in_len < SESSION_MAGIC_LEN + 16 || memcmp(in, SESSION_MAGIC, SESSION_MAGIC_LEN) != 0)
return NCM_ERR_CRYPTO;
unsigned char key[16];
if (ncm_store_machine_key(SESSION_SALT, key) != 0) return NCM_ERR_CRYPTO;
/* 用指定密钥解密已解包(魔数/iv 已剥离)的密文;成功返回 NCM_OK。 */
static int cookie_decrypt_key(const unsigned char *in, size_t in_len,
const unsigned char key[16], char *out, size_t out_sz) {
const unsigned char *iv = in + SESSION_MAGIC_LEN;
size_t clen = in_len - SESSION_MAGIC_LEN - 16;
if (clen == 0 || clen % 16) return NCM_ERR_CRYPTO;
@@ -246,6 +264,34 @@ int ncm_store_cookie_decrypt(const unsigned char *in, size_t in_len,
return NCM_OK;
}
/* 解密(*legacy 可选:回填 1=命中的是旧版 MAC 派生密钥,调用方据此迁移重加密):
* ① 新密钥(.mkey 派生)——当前写入格式;
* ② 旧密钥(MAC 派生)——兼容升级前落盘的 session.enc,解出即迁移。 */
static int cookie_decrypt_sel(const unsigned char *in, size_t in_len,
char *out, size_t out_sz, int *legacy) {
if (!in || !out || !out_sz) return NCM_ERR_ARG;
if (legacy) *legacy = 0;
if (in_len < SESSION_MAGIC_LEN + 16 || memcmp(in, SESSION_MAGIC, SESSION_MAGIC_LEN) != 0)
return NCM_ERR_CRYPTO;
unsigned char key[16];
if (session_key(SESSION_SALT, key) == 0) {
int rc = cookie_decrypt_key(in, in_len, key, out, out_sz);
if (rc == NCM_OK) return NCM_OK;
if (rc == NCM_ERR_FULL || rc == NCM_ERR_OOM) return rc;
}
if (ncm_store_machine_key(SESSION_SALT, key) == 0) {
int rc = cookie_decrypt_key(in, in_len, key, out, out_sz);
if (rc == NCM_OK) { if (legacy) *legacy = 1; return NCM_OK; }
}
return NCM_ERR_CRYPTO;
}
int ncm_store_cookie_decrypt(const unsigned char *in, size_t in_len,
char *out, size_t out_sz) {
return cookie_decrypt_sel(in, in_len, out, out_sz, NULL);
}
int ncm_store_save_session(const ncm_store_t *st, const char *cookie) {
if (!st || !cookie) return NCM_ERR_ARG;
unsigned char enc[8192];
@@ -264,7 +310,11 @@ int ncm_store_load_session(const ncm_store_t *st, char *cookie, size_t sz) {
char raw[8192];
int n = ncm_store_read_file(path, raw, sizeof raw);
if (n <= 0) return NCM_ERR_NOENT;
return ncm_store_cookie_decrypt((const unsigned char *)raw, (size_t)n, cookie, sz);
int legacy = 0;
int rc = cookie_decrypt_sel((const unsigned char *)raw, (size_t)n, cookie, sz, &legacy);
/* 旧版 MAC 密钥解出 → 就近迁移为 .mkey 密钥(失败不回滚:本次仍返回明文会话)。 */
if (rc == NCM_OK && legacy) (void)ncm_store_save_session(st, cookie);
return rc;
}
int ncm_store_clear_session(const ncm_store_t *st) {
+7 -3
View File
@@ -3,7 +3,8 @@
* 职责(纯 C,不依赖 UI/SDK):
* - 应用数据目录推断(env MEOW_DATA_DIR 优先,否则 /tmp 回退)
* - deviceId(52hex) 首生成 + 持久化(风控要求稳定)
* - 机器绑定密钥派生:sha256(MAC + 盐) → AES-128-CBC key(防裸读,非真安全)
* - 设备私有密钥派生(<data_dir>/.mkey + 盐)→ AES-128-CBC key(防裸读,非真安全);
* 旧版 MAC 派生密钥保留兼容解密/自动迁移
* - 登录 cookie 加密落盘/读回(AES-128-CBC + 随机 IV + 魔数头)
* - 原子写文件、读取、配置键读取(<dir>/<key>.cfg,对齐 SDK meow_config)
*/
@@ -34,10 +35,13 @@ int ncm_store_get_device_id(ncm_store_t *st, char out[NCM_STORE_ID_MAX]);
int ncm_store_set_device_id(ncm_store_t *st, const char id[NCM_STORE_ID_MAX]);
/* 机器绑定密钥:sha256(mac + salt) 取前 16 字节作为 AES-128 key。
* mac 取首选网卡 MAC 小写;NCM_MAC 环境变量可覆盖(测试用)。失败返回非 0。 */
* mac 取首选网卡 MAC 小写;NCM_MAC 环境变量可覆盖(测试用)。失败返回非 0。
* ⚠ 仅作旧版 session.enc 的兼容解密(见下):MAC 随网卡出现/消失而变化,勿用于新写入。 */
int ncm_store_machine_key(const char *salt, unsigned char key[16]);
/* cookie 加解密(AES-128-CBC,PKCS7)。注意:这是防裸读模糊,非真安全。 */
/* cookie 加解密(AES-128-CBC,PKCS7)。注意:这是防裸读模糊,非真安全。
* M9 起密钥改为设备私有 <data_dir>/.mkey 派生(与网络/网卡状态无关);旧版 MAC 派生
* 密文仍可解,load_session 命中旧密钥时自动重写为新密钥(透明迁移)。 */
int ncm_store_cookie_encrypt(const char *plain,
unsigned char *out, size_t *out_len, size_t cap);
int ncm_store_cookie_decrypt(const unsigned char *in, size_t in_len,