427 lines
15 KiB
C
427 lines
15 KiB
C
/* mp_auth.c — 访问级 PIN 鉴权(panel.pin)+ 内联 SHA-256 + 内存会话。 */
|
||
#include "mp.h"
|
||
|
||
time_t g_auth_ttl = AUTH_TOKEN_TTL;
|
||
#define PIN_MAX_FAILS 5 /* 连续错 PIN 锁定阈值 */
|
||
#define PIN_LOCK_SEC 300 /* 锁定 5 分钟 */
|
||
static const uint32_t mp_sha256_k[64] = {
|
||
0x428a2f98u,0x71374491u,0xb5c0fbcfu,0xe9b5dba5u,0x3956c25bu,0x59f111f1u,0x923f82a4u,0xab1c5ed5u,
|
||
0xd807aa98u,0x12835b01u,0x243185beu,0x550c7dc3u,0x72be5d74u,0x80deb1feu,0x9bdc06a7u,0xc19bf174u,
|
||
0xe49b69c1u,0xefbe4786u,0x0fc19dc6u,0x240ca1ccu,0x2de92c6fu,0x4a7484aau,0x5cb0a9dcu,0x76f988dau,
|
||
0x983e5152u,0xa831c66du,0xb00327c8u,0xbf597fc7u,0xc6e00bf3u,0xd5a79147u,0x06ca6351u,0x14292967u,
|
||
0x27b70a85u,0x2e1b2138u,0x4d2c6dfcu,0x53380d13u,0x650a7354u,0x766a0abbu,0x81c2c92eu,0x92722c85u,
|
||
0xa2bfe8a1u,0xa81a664bu,0xc24b8b70u,0xc76c51a3u,0xd192e819u,0xd6990624u,0xf40e3585u,0x106aa070u,
|
||
0x19a4c116u,0x1e376c08u,0x2748774cu,0x34b0bcb5u,0x391c0cb3u,0x4ed8aa4au,0x5b9cca4fu,0x682e6ff3u,
|
||
0x748f82eeu,0x78a5636fu,0x84c87814u,0x8cc70208u,0x90befffau,0xa4506cebu,0xbef9a3f7u,0xc67178f2u
|
||
};
|
||
static int g_pin_fails = 0;
|
||
static time_t g_pin_locked_until = 0;
|
||
static auth_sess_t g_sess[AUTH_MAX_SESS];
|
||
static int g_sess_n = 0;
|
||
#define MP_ROR(x,n) (((x) >> (n)) | ((x) << (32 - (n))))
|
||
#define MP_CH(x,y,z) (((x) & (y)) ^ (~(x) & (z)))
|
||
#define MP_MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
|
||
#define MP_BSIG0(x) (MP_ROR(x,2) ^ MP_ROR(x,13) ^ MP_ROR(x,22))
|
||
#define MP_BSIG1(x) (MP_ROR(x,6) ^ MP_ROR(x,11) ^ MP_ROR(x,25))
|
||
#define MP_SSIG0(x) (MP_ROR(x,7) ^ MP_ROR(x,18) ^ ((x) >> 3))
|
||
#define MP_SSIG1(x) (MP_ROR(x,17) ^ MP_ROR(x,19) ^ ((x) >> 10))
|
||
|
||
|
||
void mp_sha256_init(mp_sha256_ctx *c)
|
||
{
|
||
c->state[0] = 0x6a09e667u; c->state[1] = 0xbb67ae85u;
|
||
c->state[2] = 0x3c6ef372u; c->state[3] = 0xa54ff53au;
|
||
c->state[4] = 0x510e527fu; c->state[5] = 0x9b05688cu;
|
||
c->state[6] = 0x1f83d9abu; c->state[7] = 0x5be0cd19u;
|
||
c->count = 0; c->buflen = 0;
|
||
}
|
||
|
||
void mp_sha256_block(mp_sha256_ctx *c, const uint8_t *p)
|
||
{
|
||
uint32_t w[64];
|
||
int i;
|
||
for (i = 0; i < 16; i++)
|
||
w[i] = ((uint32_t)p[i*4] << 24) | ((uint32_t)p[i*4+1] << 16) |
|
||
((uint32_t)p[i*4+2] << 8) | (uint32_t)p[i*4+3];
|
||
for (i = 16; i < 64; i++)
|
||
w[i] = MP_SSIG1(w[i-2]) + w[i-7] + MP_SSIG0(w[i-15]) + w[i-16];
|
||
uint32_t a = c->state[0], b = c->state[1], cc = c->state[2], d = c->state[3];
|
||
uint32_t e = c->state[4], f = c->state[5], g = c->state[6], h = c->state[7];
|
||
for (i = 0; i < 64; i++) {
|
||
uint32_t t1 = h + MP_BSIG1(e) + MP_CH(e,f,g) + mp_sha256_k[i] + w[i];
|
||
uint32_t t2 = MP_BSIG0(a) + MP_MAJ(a,b,cc);
|
||
h = g; g = f; f = e; e = d + t1;
|
||
d = cc; cc = b; b = a; a = t1 + t2;
|
||
}
|
||
c->state[0] += a; c->state[1] += b; c->state[2] += cc; c->state[3] += d;
|
||
c->state[4] += e; c->state[5] += f; c->state[6] += g; c->state[7] += h;
|
||
}
|
||
|
||
void mp_sha256_update(mp_sha256_ctx *c, const uint8_t *data, size_t len)
|
||
{
|
||
c->count += (uint64_t)len;
|
||
if (c->buflen) {
|
||
size_t need = 64 - c->buflen;
|
||
size_t take = len < need ? len : need;
|
||
memcpy(c->buf + c->buflen, data, take);
|
||
c->buflen += take; data += take; len -= take;
|
||
if (c->buflen == 64) { mp_sha256_block(c, c->buf); c->buflen = 0; }
|
||
}
|
||
while (len >= 64) { mp_sha256_block(c, data); data += 64; len -= 64; }
|
||
if (len) { memcpy(c->buf, data, len); c->buflen = len; }
|
||
}
|
||
|
||
void mp_sha256_final(mp_sha256_ctx *c, uint8_t out[32])
|
||
{
|
||
uint64_t bitlen = c->count * 8;
|
||
uint8_t *b = c->buf;
|
||
size_t bl = c->buflen;
|
||
int i;
|
||
b[bl++] = 0x80;
|
||
if (bl > 56) {
|
||
while (bl < 64) b[bl++] = 0;
|
||
mp_sha256_block(c, b);
|
||
bl = 0;
|
||
}
|
||
while (bl < 56) b[bl++] = 0;
|
||
for (i = 0; i < 8; i++)
|
||
b[bl++] = (uint8_t)(bitlen >> (56 - i * 8));
|
||
mp_sha256_block(c, b);
|
||
for (i = 0; i < 8; i++) {
|
||
out[i*4] = (uint8_t)(c->state[i] >> 24);
|
||
out[i*4+1] = (uint8_t)(c->state[i] >> 16);
|
||
out[i*4+2] = (uint8_t)(c->state[i] >> 8);
|
||
out[i*4+3] = (uint8_t)(c->state[i]);
|
||
}
|
||
}
|
||
|
||
int auth_read_random(unsigned char *buf, size_t n)
|
||
{
|
||
int fd = open("/dev/urandom", O_RDONLY);
|
||
if (fd < 0) return -1;
|
||
size_t got = 0;
|
||
while (got < n) {
|
||
ssize_t rd = read(fd, buf + got, n - got);
|
||
if (rd <= 0) { close(fd); return -1; }
|
||
got += (size_t)rd;
|
||
}
|
||
close(fd);
|
||
return 0;
|
||
}
|
||
|
||
void auth_hex(const unsigned char *raw, size_t n, char *out)
|
||
{
|
||
static const char hex[] = "0123456789abcdef";
|
||
for (size_t i = 0; i < n; i++) {
|
||
out[i*2] = hex[raw[i] >> 4];
|
||
out[i*2+1] = hex[raw[i] & 0xf];
|
||
}
|
||
out[n*2] = 0;
|
||
}
|
||
|
||
int auth_token_gen(char *out, int outcap) /* 16B → 32 hex(128bit) */
|
||
{
|
||
unsigned char raw[AUTH_TOKEN_BYTES];
|
||
if (outcap < AUTH_TOKEN_BYTES * 2 + 1) return -1;
|
||
if (auth_read_random(raw, sizeof raw) < 0) return -1;
|
||
auth_hex(raw, sizeof raw, out);
|
||
return 0;
|
||
}
|
||
|
||
int auth_make_salt(char *out, int outcap) /* 16B → 32 hex 盐 */
|
||
{
|
||
unsigned char raw[16];
|
||
if (outcap < 33) return -1;
|
||
if (auth_read_random(raw, sizeof raw) < 0) return -1;
|
||
auth_hex(raw, sizeof raw, out);
|
||
return 0;
|
||
}
|
||
|
||
int auth_pass_hash(const char *salt, const char *password, char *out, int outcap)
|
||
{
|
||
mp_sha256_ctx c;
|
||
uint8_t digest[32];
|
||
if (outcap < AUTH_HASH_LEN + 1) return -1;
|
||
mp_sha256_init(&c);
|
||
mp_sha256_update(&c, (const uint8_t *)salt, strlen(salt));
|
||
mp_sha256_update(&c, (const uint8_t *)password, strlen(password));
|
||
mp_sha256_final(&c, digest);
|
||
auth_hex(digest, sizeof digest, out);
|
||
return 0;
|
||
}
|
||
|
||
int auth_make_hash(const char *password, char *out, int outcap) /* salt:hex */
|
||
{
|
||
char salt[33], h[AUTH_HASH_LEN + 1];
|
||
if (auth_make_salt(salt, sizeof salt) < 0) return -1;
|
||
if (auth_pass_hash(salt, password, h, sizeof h) < 0) return -1;
|
||
snprintf(out, outcap, "%s:%s", salt, h);
|
||
return 0;
|
||
}
|
||
|
||
int hash_matches(const char *stored, const char *password)
|
||
{
|
||
if (!stored || !stored[0] || !password) return 0;
|
||
char tmp[160];
|
||
snprintf(tmp, sizeof tmp, "%s", stored);
|
||
char *colon = strchr(tmp, ':');
|
||
if (!colon) return 0;
|
||
*colon = 0;
|
||
char want[AUTH_HASH_LEN + 1];
|
||
if (auth_pass_hash(tmp, password, want, sizeof want) != 0) return 0;
|
||
if (strlen(colon + 1) != AUTH_HASH_LEN) return 0;
|
||
return memcmp(want, colon + 1, AUTH_HASH_LEN) == 0;
|
||
}
|
||
|
||
int pin_set(void) /* panel.pin 是否已设(访问屏障开启) */
|
||
{
|
||
char *v = settings_get_value("panel.pin");
|
||
int s = (v && v[0]) ? 1 : 0;
|
||
free(v);
|
||
return s;
|
||
}
|
||
|
||
int auth_pass_set(void) /* 旧密码是否已设(兼容迁移用) */
|
||
{
|
||
char *v = settings_get_value("panel.auth_pass");
|
||
int s = (v && v[0]) ? 1 : 0;
|
||
free(v);
|
||
return s;
|
||
}
|
||
|
||
int auth_verify(const char *password) /* 旧密码校验(迁移期;见 login) */
|
||
{
|
||
char *stored = settings_get_value("panel.auth_pass");
|
||
int ok = hash_matches(stored, password);
|
||
free(stored);
|
||
return ok;
|
||
}
|
||
|
||
int pin_verify(const char *pin) /* PIN 校验 */
|
||
{
|
||
char *stored = settings_get_value("panel.pin");
|
||
int ok = hash_matches(stored, pin);
|
||
free(stored);
|
||
return ok;
|
||
}
|
||
|
||
int pin_validate(const char *pin)
|
||
{
|
||
if (!pin) return 0;
|
||
size_t l = strlen(pin);
|
||
if (l != 4) return 0;
|
||
for (size_t i = 0; i < l; i++)
|
||
if (pin[i] < '0' || pin[i] > '9') return 0;
|
||
return 1;
|
||
}
|
||
|
||
long long pin_lock_remaining(void) /* 剩余锁定秒;0=未锁 */
|
||
{
|
||
time_t now = time(NULL);
|
||
if (g_pin_locked_until <= now) return 0;
|
||
return (long long)(g_pin_locked_until - now);
|
||
}
|
||
|
||
void pin_lock_reset(void)
|
||
{
|
||
g_pin_fails = 0;
|
||
g_pin_locked_until = 0;
|
||
}
|
||
|
||
void pin_record_fail(void)
|
||
{
|
||
if (++g_pin_fails >= PIN_MAX_FAILS) {
|
||
g_pin_locked_until = time(NULL) + PIN_LOCK_SEC;
|
||
g_pin_fails = 0;
|
||
}
|
||
}
|
||
|
||
int sess_find(const char *token)
|
||
{
|
||
for (int i = 0; i < g_sess_n; i++)
|
||
if (!strcmp(g_sess[i].token, token)) return i;
|
||
return -1;
|
||
}
|
||
|
||
int sess_valid(const char *token)
|
||
{
|
||
int i = sess_find(token);
|
||
if (i < 0) return 0;
|
||
if (g_sess[i].expires > 0 && time(NULL) >= g_sess[i].expires) return 0;
|
||
return 1;
|
||
}
|
||
|
||
void sess_add(const char *token, time_t expires)
|
||
{
|
||
int i = sess_find(token);
|
||
if (i >= 0) { g_sess[i].expires = expires; return; }
|
||
if (g_sess_n >= AUTH_MAX_SESS) { /* 满:淘汰最早过期的一条 */
|
||
int e = 0;
|
||
for (int k = 1; k < g_sess_n; k++)
|
||
if (g_sess[k].expires < g_sess[e].expires) e = k;
|
||
if (e < g_sess_n - 1)
|
||
memmove(&g_sess[e], &g_sess[e + 1], (size_t)(g_sess_n - 1 - e) * sizeof(auth_sess_t));
|
||
g_sess_n--;
|
||
}
|
||
snprintf(g_sess[g_sess_n].token, sizeof g_sess[g_sess_n].token, "%s", token);
|
||
g_sess[g_sess_n].expires = expires;
|
||
g_sess_n++;
|
||
}
|
||
|
||
void sess_remove(const char *token)
|
||
{
|
||
int i = sess_find(token);
|
||
if (i < 0) return;
|
||
if (i < g_sess_n - 1)
|
||
memmove(&g_sess[i], &g_sess[i + 1], (size_t)(g_sess_n - 1 - i) * sizeof(auth_sess_t));
|
||
g_sess_n--;
|
||
}
|
||
|
||
void auth_cp(char *dst, int dcap, const char *src) /* 有界拷贝(防 -Wformat-truncation) */
|
||
{
|
||
size_t l = strlen(src);
|
||
if (l >= (size_t)dcap) l = (size_t)dcap - 1;
|
||
memcpy(dst, src, l);
|
||
dst[l] = 0;
|
||
}
|
||
|
||
int extract_token(const req_t *r, char *out, int outcap)
|
||
{
|
||
out[0] = 0;
|
||
if (r->x_meow_token[0]) { auth_cp(out, outcap, r->x_meow_token); return out[0] ? 1 : 0; }
|
||
if (!strncasecmp(r->authorization, "Bearer ", 7)) {
|
||
auth_cp(out, outcap, r->authorization + 7);
|
||
return out[0] ? 1 : 0;
|
||
}
|
||
if (r->cookie[0]) {
|
||
const char *p = strstr(r->cookie, "meow_token=");
|
||
if (p) {
|
||
p += 11;
|
||
const char *e = p;
|
||
while (*e && *e != ';' && *e != ' ' && *e != '\t') e++;
|
||
size_t l = (size_t)(e - p);
|
||
if (l >= (size_t)outcap) l = (size_t)outcap - 1;
|
||
memcpy(out, p, l);
|
||
out[l] = 0;
|
||
return out[0] ? 1 : 0;
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
int require_auth(int sock, const req_t *r)
|
||
{
|
||
const char *p = r->path;
|
||
if (!strcmp(p, "/api/auth/login") || !strcmp(p, "/api/auth/logout") ||
|
||
!strcmp(p, "/api/auth/status") || !strcmp(p, "/api/auth/setup")) return 1;
|
||
char t[65];
|
||
if (extract_token(r, t, sizeof t) && t[0] && sess_valid(t)) return 1;
|
||
return send_err(sock, 401, "UNAUTHORIZED", "未认证或会话已失效");
|
||
}
|
||
|
||
int api_auth_status(int sock, const req_t *r)
|
||
{
|
||
int ps = pin_set();
|
||
int lg = auth_pass_set();
|
||
int sess = 0;
|
||
char t[65];
|
||
if (extract_token(r, t, sizeof t) && t[0]) sess = sess_valid(t);
|
||
long long rem = pin_lock_remaining();
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"auth_enabled\":true,\"session\":%s,\"pin_set\":%s,\"legacy\":%s,\"locked\":%s",
|
||
sess ? "true" : "false", ps ? "true" : "false", lg ? "true" : "false", rem > 0 ? "true" : "false");
|
||
if (rem > 0) sA(&s, ",\"lock_remaining_ms\":%lld", rem * 1000LL);
|
||
sA(&s, "}");
|
||
int rc = send_json(sock, 200, s.b ? s.b : "{}");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
int api_auth_login(int sock, const req_t *r)
|
||
{
|
||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 PIN");
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
jv_t *jpin = jv_obj_get(root, "pin");
|
||
jv_t *jpw = jv_obj_get(root, "password");
|
||
char pin[8], pw[256];
|
||
if (jpin && jpin->type == J_STR && jpin->str) snprintf(pin, sizeof pin, "%s", jpin->str); else pin[0] = 0;
|
||
if (jpw && jpw->type == J_STR && jpw->str) snprintf(pw, sizeof pw, "%s", jpw->str); else pw[0] = 0;
|
||
jv_free(root);
|
||
|
||
if (pin_lock_remaining() > 0) { /* 已在锁定期 */
|
||
sbuf_t e = {0};
|
||
sA(&e, "{\"ok\":false,\"error\":{\"code\":\"LOCKED\",\"message\":\"已锁定,请稍后重试\"},\"lock_remaining_ms\":%lld}",
|
||
pin_lock_remaining() * 1000LL);
|
||
int rc = send_json(sock, 429, e.b ? e.b : "");
|
||
sbuf_free(&e);
|
||
return rc;
|
||
}
|
||
|
||
int use_legacy = 0;
|
||
const char *cred = NULL;
|
||
if (pin_set()) { cred = pin; if (!cred || !cred[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 pin 字段"); }
|
||
else if (auth_pass_set()) { use_legacy = 1; cred = pw; if (!cred || !cred[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 password 字段"); }
|
||
else return send_err(sock, 400, "BAD_REQUEST", "请先设置面板 PIN(/api/auth/setup)");
|
||
|
||
int ok = use_legacy ? auth_verify(cred) : pin_verify(cred);
|
||
if (!ok) {
|
||
pin_record_fail();
|
||
if (pin_lock_remaining() > 0) { /* 本错已触发锁定 */
|
||
sbuf_t e = {0};
|
||
sA(&e, "{\"ok\":false,\"error\":{\"code\":\"LOCKED\",\"message\":\"连续错误次数过多,已锁定 5 分钟\"},\"lock_remaining_ms\":%lld}",
|
||
pin_lock_remaining() * 1000LL);
|
||
int rc = send_json(sock, 429, e.b ? e.b : "");
|
||
sbuf_free(&e);
|
||
return rc;
|
||
}
|
||
return send_err(sock, 401, "UNAUTHORIZED", "PIN 错误");
|
||
}
|
||
pin_lock_reset();
|
||
char token[65];
|
||
if (auth_token_gen(token, sizeof token) < 0) return send_err(sock, 500, "INTERNAL", "生成会话失败");
|
||
time_t exp = time(NULL) + g_auth_ttl; /* 固定 3 天(MEOW_AUTH_TTL 可覆盖测试用),不滑动续期 */
|
||
sess_add(token, exp);
|
||
sbuf_t s = {0};
|
||
sA(&s, "{\"token\":");
|
||
sA_str(&s, token);
|
||
sA(&s, ",\"expires_at\":%lld,\"auth_enabled\":true}", (long long)exp);
|
||
int rc = send_json(sock, 200, s.b ? s.b : "{}");
|
||
sbuf_free(&s);
|
||
return rc;
|
||
}
|
||
|
||
int api_auth_setup(int sock, const req_t *r)
|
||
{
|
||
if (pin_set()) return send_err(sock, 409, "CONFLICT", "面板 PIN 已设置,请直接登录");
|
||
if (!r->body || !r->body[0]) return send_err(sock, 400, "BAD_REQUEST", "缺少 PIN");
|
||
jv_t *root = jv_parse(r->body, (int)r->body_len);
|
||
if (!root) return send_err(sock, 400, "BAD_REQUEST", "body 非合法 JSON");
|
||
jv_t *jpin = jv_obj_get(root, "pin");
|
||
char pin[8];
|
||
if (jpin && jpin->type == J_STR && jpin->str) snprintf(pin, sizeof pin, "%s", jpin->str);
|
||
else pin[0] = 0;
|
||
jv_free(root);
|
||
if (!pin_validate(pin)) return send_err(sock, 422, "UNSUPPORTED", "PIN 必须为 4 位数字");
|
||
char h[200];
|
||
if (auth_make_hash(pin, h, sizeof h) < 0) return send_err(sock, 500, "INTERNAL", "生成 PIN 哈希失败");
|
||
if (settings_write_one("panel.pin", h) < 0) return send_err(sock, 500, "INTERNAL", "写设置失败");
|
||
pin_lock_reset();
|
||
return send_json(sock, 200, "{\"pin_set\":true}");
|
||
}
|
||
|
||
int api_auth_logout(int sock, const req_t *r)
|
||
{
|
||
char t[65];
|
||
if (extract_token(r, t, sizeof t) && t[0]) sess_remove(t);
|
||
return send_json(sock, 200, "{\"logged_out\":true}");
|
||
}
|
||
|
||
#undef MP_ROR
|
||
#undef MP_CH
|
||
#undef MP_MAJ
|
||
#undef MP_BSIG0
|
||
#undef MP_BSIG1
|
||
#undef MP_SSIG0
|
||
#undef MP_SSIG1
|