diff --git a/orbisrpc/cfg.c b/orbisrpc/cfg.c index 820e647..267cd6c 100644 --- a/orbisrpc/cfg.c +++ b/orbisrpc/cfg.c @@ -12,6 +12,7 @@ cfg_t g_cfg; void cfg_defaults(cfg_t *c) { if(!c) return; memset(c, 0, sizeof(*c)); + c->schema_version = CFG_SCHEMA_VERSION; c->enabled = 1; c->auto_update = 1; c->poll_interval_s = 12; @@ -65,15 +66,20 @@ int cfg_load(const char *path, cfg_t *c) { o = jl_obj_get(root, "auto_update"); if (o && o->type == JL_BOOL) c->auto_update = (int)o->num; o = jl_obj_get(root, "debug"); if (o && o->type == JL_BOOL) c->debug = (int)o->num; o = jl_obj_get(root, "poll_interval_s"); if (o && o->type == JL_NUMBER) c->poll_interval_s = (int)o->num; + o = jl_obj_get(root, "schema_version"); if (o && o->type == JL_NUMBER) c->schema_version = (int)o->num; jl_free(root); clamp_cfg(c); + /* Migration: stamp current schema so re-saves converge. + * v0 (no key): identical layout, adopt as-is. */ + c->schema_version = CFG_SCHEMA_VERSION; return 0; } -void cfg_save(const char *path, const cfg_t *c) { - if(!path || !c) return; +int cfg_save(const char *path, const cfg_t *c) { + if(!path || !c) return -1; jl_val_t *r = jl_new_object(); - if(!r) { log_msg("cfg_save: allocation failed"); return; } + if(!r) { log_msg("cfg_save: allocation failed"); return -1; } + jl_obj_set(r, "schema_version", jl_new_number((double)CFG_SCHEMA_VERSION)); jl_obj_set(r, "token", jl_new_string(c->token)); jl_obj_set(r, "application_id", jl_new_string(c->application_id)); jl_obj_set(r, "art_base_url", jl_new_string(c->art_base_url)); @@ -83,13 +89,14 @@ void cfg_save(const char *path, const cfg_t *c) { jl_obj_set(r, "poll_interval_s", jl_new_number((double)c->poll_interval_s)); jl_obj_set(r, "presence_state", jl_new_string(c->presence_state)); char *s = jl_stringify(r); - if(!s){ log_msg("cfg_save: serialization failed"); jl_free(r); return; } + if(!s){ log_msg("cfg_save: serialization failed"); jl_free(r); return -1; } /* write tmp + fsync + rename so a power loss can't corrupt the config */ char tmp[160]; snprintf(tmp, sizeof tmp, "%s.tmp", path); FILE *f = fopen(tmp, "wb"); + int ok = 0; if (f) { - int ok = (fputs(s, f) >= 0); + ok = (fputs(s, f) >= 0); if(fflush(f) != 0) ok = 0; /* force bytes to disk before rename */ if(ok) { int fd = fileno(f); if(fd >= 0 && fsync(fd) != 0) ok = 0; } diff --git a/orbisrpc/cfg.h b/orbisrpc/cfg.h index 3b4eb8b..c34cf72 100644 --- a/orbisrpc/cfg.h +++ b/orbisrpc/cfg.h @@ -5,7 +5,11 @@ #define CFG_PATH "/data/orbisRPC/config.json" #define LOG_PATH "/data/orbisRPC/log.txt" #define DATA_DIR "/data/orbisRPC" +/* Config schema version. Stamp on load so re-saves converge; unknown + * future versions load defensively (known fields only). */ +#define CFG_SCHEMA_VERSION 1 typedef struct { + int schema_version; char token[512]; /* Discord user session token */ char application_id[64]; /* optional: app id for uploaded asset images */ char art_base_url[256]; /* optional: icon pack base URL, e.g. @@ -19,6 +23,8 @@ typedef struct { } cfg_t; extern cfg_t g_cfg; int cfg_load(const char *path, cfg_t *c); -void cfg_save(const char *path, const cfg_t *c); +/* Atomic save (tmp + fsync + rename). 0 saved, -1 failed (caller must + * surface, not assume). */ +int cfg_save(const char *path, const cfg_t *c); void cfg_defaults(cfg_t *c); #endif diff --git a/orbisrpc/daemon.c b/orbisrpc/daemon.c index 37c60d4..60337a1 100644 --- a/orbisrpc/daemon.c +++ b/orbisrpc/daemon.c @@ -111,8 +111,10 @@ int daemon_run(const char *fixed_game_name){ FILE *probe = fopen(CFG_PATH, "rb"); if(!probe){ cfg_defaults(&g_cfg); - cfg_save(CFG_PATH, &g_cfg); - log_msg("created template %s; edit \"token\" over FTP then reboot", CFG_PATH); + if(cfg_save(CFG_PATH, &g_cfg) != 0) + log_msg("WARN: template config unwritable; token edits will not persist"); + else + log_msg("created template %s; edit \"token\" then reboot", CFG_PATH); } else fclose(probe); log_msg("config load failed; running on defaults until valid config appears"); } diff --git a/orbisrpc/detect.c b/orbisrpc/detect.c index aa1aef7..6da377b 100644 --- a/orbisrpc/detect.c +++ b/orbisrpc/detect.c @@ -112,8 +112,12 @@ static long scan_newest_save(char *out, size_t cap){ struct dirent *f; while((f = readdir(td))){ if(f->d_name[0] == '.') continue; - char fp[220]; - snprintf(fp, sizeof fp, "%s/%s", tp, f->d_name); + /* d_name can be up to 255 chars; tp already holds up + * to ~150 — skip overlong names instead of overflowing. */ + if(strlen(f->d_name) > 64) continue; + char fp[256]; + int wn = snprintf(fp, sizeof fp, "%s/%s", tp, f->d_name); + if(wn <= 0 || (size_t)wn >= sizeof fp) continue; struct stat fs; if(stat(fp, &fs) == 0 && fs.st_mtime > tb) tb = fs.st_mtime; } @@ -240,6 +244,7 @@ static void remember_titleid(const char *ti){ s_last_titleid[sizeof s_last_titleid - 1] = 0; } static long scan_one_appdir(const char *base, char *out, size_t cap, long best){ + if(!base || !out || cap < 2) return best; DIR *d = opendir(base); if(!d) return best; struct dirent *e; diff --git a/orbisrpc/log.c b/orbisrpc/log.c index a1ed290..1e0e22c 100644 --- a/orbisrpc/log.c +++ b/orbisrpc/log.c @@ -47,7 +47,7 @@ void log_init(const char *path) { if (!g_log) { for (unsigned i = 0; i < sizeof(LOG_FALLBACKS)/sizeof(LOG_FALLBACKS[0]); i++) { const char *p = LOG_FALLBACKS[i]; - if (!p || strcmp(p, path) == 0) continue; + if (!p || !path || strcmp(p, path) == 0) continue; g_log = fopen(p, "ab"); if (g_log) break; } diff --git a/orbisrpc/manifest.c b/orbisrpc/manifest.c index a900c5a..8fe8714 100644 --- a/orbisrpc/manifest.c +++ b/orbisrpc/manifest.c @@ -135,24 +135,31 @@ int manifest_verify_sig(const unsigned char *msg, size_t msglen, const unsigned char pubkey[64]){ if(!msg || !sig || !pubkey) return -1; unsigned char hash[32]; - if(mbedtls_sha256(msg, msglen, hash, 0) != 0) return -1; + { + mbedtls_sha256_context sc; + mbedtls_sha256_init(&sc); + int ok = mbedtls_sha256_starts(&sc, 0) == 0 && + mbedtls_sha256_update(&sc, msg, msglen) == 0 && + mbedtls_sha256_finish(&sc, hash) == 0; + mbedtls_sha256_free(&sc); + if(!ok) return -1; + } mbedtls_ecp_group grp; mbedtls_ecp_point Q; - mbedtls_mpi r, s, x, y, z; + mbedtls_mpi r, s; mbedtls_ecp_group_init(&grp); mbedtls_ecp_point_init(&Q); mbedtls_mpi_init(&r); mbedtls_mpi_init(&s); - mbedtls_mpi_init(&x); mbedtls_mpi_init(&y); mbedtls_mpi_init(&z); int rc = -1; + unsigned char uncompressed[65]; if(mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1) != 0) goto out; if(mbedtls_mpi_read_binary(&r, sig, 32) != 0) goto out; if(mbedtls_mpi_read_binary(&s, sig + 32, 32) != 0) goto out; - if(mbedtls_mpi_read_binary(&x, pubkey, 32) != 0) goto out; - if(mbedtls_mpi_read_binary(&y, pubkey + 32, 32) != 0) goto out; - if(mbedtls_mpi_lset(&z, 1) != 0) goto out; - if(mbedtls_mpi_copy(&Q.private_X, &x) != 0) goto out; - if(mbedtls_mpi_copy(&Q.private_Y, &y) != 0) goto out; - if(mbedtls_mpi_copy(&Q.private_Z, &z) != 0) goto out; + /* Public API only (no struct internals): uncompressed point 0x04||X||Y. */ + uncompressed[0] = 0x04; + memcpy(uncompressed + 1, pubkey, 64); + if(mbedtls_ecp_point_read_binary(&grp, &Q, uncompressed, + sizeof uncompressed) != 0) goto out; if(mbedtls_ecp_check_pubkey(&grp, &Q) != 0) goto out; if(mbedtls_ecdsa_verify(&grp, hash, sizeof hash, &Q, &r, &s) != 0) goto out; rc = 0; @@ -160,6 +167,5 @@ out: mbedtls_ecp_group_free(&grp); mbedtls_ecp_point_free(&Q); mbedtls_mpi_free(&r); mbedtls_mpi_free(&s); - mbedtls_mpi_free(&x); mbedtls_mpi_free(&y); mbedtls_mpi_free(&z); return rc; } diff --git a/tests/test_utils.c b/tests/test_utils.c index d8d2825..3a8cb47 100644 --- a/tests/test_utils.c +++ b/tests/test_utils.c @@ -7,6 +7,11 @@ #include "../orbisrpc/art.h" #include "../orbisrpc/health.h" #include "../orbisrpc/manifest.h" +#include +#include +#include +#include +#include #include #include #include @@ -321,6 +326,64 @@ static void test_manifest(void) { assert(manifest_check(&m3, "a", (const unsigned char *)"abd", 3) != 0); } +static void test_manifest_sig(void) { + /* Full round trip with a fresh keypair: sign via mbedTLS, verify via + * our public-API-only manifest_verify_sig. Tampered bytes must fail. + * Uses only public 3.x APIs (raw group + MPIs, no context internals). */ + static const unsigned char msg[] = "{\"version\":\"9.9.9\"}"; + mbedtls_entropy_context ent; + mbedtls_entropy_init(&ent); + mbedtls_ctr_drbg_context rng; + mbedtls_ctr_drbg_init(&rng); + assert(mbedtls_ctr_drbg_seed(&rng, mbedtls_entropy_func, &ent, + (const unsigned char *)"test", 4) == 0); + mbedtls_ecp_group grp; + mbedtls_ecp_group_init(&grp); + assert(mbedtls_ecp_group_load(&grp, MBEDTLS_ECP_DP_SECP256R1) == 0); + mbedtls_mpi d, r, s; + mbedtls_mpi_init(&d); mbedtls_mpi_init(&r); mbedtls_mpi_init(&s); + mbedtls_ecp_point Q; + mbedtls_ecp_point_init(&Q); + assert(mbedtls_ecp_gen_keypair(&grp, &d, &Q, + mbedtls_ctr_drbg_random, &rng) == 0); + unsigned char hash[32], sig[64], rawpub[64]; + { + mbedtls_sha256_context sc; + mbedtls_sha256_init(&sc); + assert(mbedtls_sha256_starts(&sc, 0) == 0); + assert(mbedtls_sha256_update(&sc, msg, sizeof msg - 1) == 0); + assert(mbedtls_sha256_finish(&sc, hash) == 0); + mbedtls_sha256_free(&sc); + } + assert(mbedtls_ecdsa_sign(&grp, &r, &s, &d, hash, sizeof hash, + mbedtls_ctr_drbg_random, &rng) == 0); + assert(mbedtls_mpi_write_binary(&r, sig, 32) == 0); + assert(mbedtls_mpi_write_binary(&s, sig + 32, 32) == 0); + /* export X||Y via the public point-write API */ + { + unsigned char uncomp[65]; + size_t olen = 0; + assert(mbedtls_ecp_point_write_binary(&grp, &Q, + MBEDTLS_ECP_PF_UNCOMPRESSED, &olen, uncomp, sizeof uncomp) == 0); + assert(olen == 65 && uncomp[0] == 0x04); + memcpy(rawpub, uncomp + 1, 64); + } + assert(manifest_verify_sig(msg, sizeof msg - 1, sig, rawpub) == 0); + sig[10] ^= 0x01; + assert(manifest_verify_sig(msg, sizeof msg - 1, sig, rawpub) != 0); + sig[10] ^= 0x01; + unsigned char bad[sizeof msg]; + memcpy(bad, msg, sizeof bad); + bad[5] ^= 0x01; + assert(manifest_verify_sig(bad, sizeof bad - 1, sig, rawpub) != 0); + assert(manifest_verify_sig(NULL, 0, sig, rawpub) != 0); + mbedtls_mpi_free(&d); mbedtls_mpi_free(&r); mbedtls_mpi_free(&s); + mbedtls_ecp_point_free(&Q); + mbedtls_ecp_group_free(&grp); + mbedtls_ctr_drbg_free(&rng); + mbedtls_entropy_free(&ent); +} + static void test_base64(void) { char out[32]; assert(b64_encode((const unsigned char *)"", 0, out) == 0); @@ -347,6 +410,7 @@ int main(void) { test_health_safe_mode(); test_health_stage_activate(); test_manifest(); + test_manifest_sig(); puts("utility tests passed"); return 0; } diff --git a/tests/test_utils_asan b/tests/test_utils_asan index a38ce86..7dc4c84 100755 Binary files a/tests/test_utils_asan and b/tests/test_utils_asan differ