diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bf79f1..cd68a20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,8 @@ jobs: run: make -C tests asan - name: e2e contracts run: python3 tests/e2e_consumer.py + env: + ORBISRPC_STRICT_SECRETS: "1" sdk-payload: runs-on: ubuntu-latest steps: diff --git a/.gitignore b/.gitignore index 32d24b0..679a8c1 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ installer/pkg.gp4 installer/assets/daemon.elf installer/assets/evict.elf installer/sce_sys/param.sfo +tests/test_utils_asan diff --git a/README.md b/README.md index 36edd74..fca3139 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ orbisRPC

+# orbisRPC โ€” Discord Rich Presence for PS4 (GoldHEN RPC) +

version PS4 GoldHEN @@ -20,22 +22,24 @@ playing to Discord: name, cover art, timer. No PC at runtime.

1. Grab `OrbisRPC-Setup-1.0.0.pkg` from the [Releases page](https://github.com/SirHumza/orbisRPC/releases/tag/v1.0.0) and install it with Package Installer. -2. Open **orbisRPC Setup** โ†’ say Yes. It drops the daemon into GoldHEN's - `bin/elf` and saves your Discord token. -3. Start **orbisrpc** from GoldHEN's payload menu, launch a game, watch Discord. +2. Open **orbisRPC Setup**. It stages `orbisrpc.bin` + `evict.elf` in + `/data/payloads`, writes `/data/orbisRPC/config.json`, evicts any old + daemon, then asks for your Discord token. +3. Launch **orbisrpc** from Payload Guest (GoldHEN's payload menu), + launch a game, watch Discord. -After a reboot: re-jailbreak, then enable AutoRun for `orbisrpc` in GoldHEN's -payload menu once โ€” it starts itself on every jailbreak after that. +After a reboot: re-jailbreak, then enable AutoRun for `orbisrpc` in +Payload Guest once โ€” it starts itself on every jailbreak after that. ## What you get | | | |---|---| -| ๐ŸŽฎ **Any game, no lists** | Names resolve from your console's own database โ€” CUSA, PPSA, indies, all covered with zero per-game setup. | +| ๐ŸŽฎ **Any game, no lists** | Names resolve from your console's metadata (SFO, app.xml) plus Sony's TMDB โ€” CUSA, PPSA, indies, zero per-game setup. | | ๐Ÿ–ผ๏ธ **Real cover art** | Game art served per title, PlayStation logo when idle. | | โฑ๏ธ **True timers** | Survive reconnects and restarts, resume across quick game switches. | | ๐Ÿง  **Self-learning** | First-seen titles are remembered, so later boots resolve instantly. | -| ๐Ÿ”„ **Self-updating** | Signed daemon updates with boot rollback. No reinstall treadmill. | +| ๐Ÿ”„ **Self-updating** | Daemon updates land from GitHub releases with automatic rollback. No reinstall treadmill. | | ๐Ÿ“ฆ **One-tap installer** | Setup PKG: install โ†’ token โ†’ payload in place. | ## How it works @@ -45,7 +49,7 @@ PS4 (GoldHEN) Discord โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ orbisRPC daemon โ”‚ TLS โ”‚ your profile โ”‚ โ”‚ sandbox scan โ†’ game ID โ”‚ โ—„โ”€โ”€โ–บ โ”‚ Playing Game โ”‚ -โ”‚ app.db โ†’ display name โ”‚ โ”‚ [cover] [timer] โ”‚ +โ”‚ metadata/TMDB โ†’ name โ”‚ โ”‚ [cover] [timer] โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index f791e8a..e6d8ea0 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -28,12 +28,12 @@ predates the fix โ€” reinstall. ## Payload won't start -The Setup PKG only writes `/data/GoldHEN/bin/elf/orbisrpc.bin` โ€” it never -launches anything. Start it from GoldHEN's payload menu, or enable AutoRun -for `orbisrpc` once so it boots with every jailbreak. +The Setup PKG only writes `/data/payloads/orbisrpc.bin` โ€” it never +launches anything. Start it from Payload Guest (GoldHEN's payload menu), +or enable AutoRun for `orbisrpc` once so it boots with every jailbreak. Manual injection (see `injecting.md`) still exists and needs the loaders -open: ports 9090/9021/9020 are closed whenever GoldHEN's BinLoader toggle +open: ports 9021/9020 are closed whenever GoldHEN's BinLoader toggle is off. Flip it in GoldHEN's menu. Reboot wipes jailbreak + daemon (RAM-only), so re-jailbreak first. diff --git a/docs/injecting.md b/docs/injecting.md index ca2aa69..2e1a4b8 100644 --- a/docs/injecting.md +++ b/docs/injecting.md @@ -65,13 +65,7 @@ GoldHEN's BinLoader arms **once**. Any empty port check (`connect_ex`, arm** โ€” the real payload then arrives at a dead listener. Sequence is always: tap โ†’ say go โ†’ fire immediately โ†’ verify. Never probe first. -## Method 3 โ€” in-app injector - -The OrbisRPC setup app injects over loopback itself -(`127.0.0.1:9020`, fallback `9090`) from its menu. Needs a listening -loader exactly like Method 2. - -## Method 4 โ€” sender page (no PC tools) +## Method 3 โ€” sender page (no PC tools) `https://SirHumza.github.io/orbisrpc-host/` in the PS4 browser auto-sends the bundled backend to the console's own loader and prints @@ -91,7 +85,7 @@ the result on screen. | Symptom | Meaning | Fix | |---|---|---| -| `Connection refused` on 9020/9090 | No listener armed | Tap BinLoader / open payloader page, retry instantly | +| `Connection refused` on 9021/9020 | No listener armed | Tap BinLoader / open payloader page, retry instantly | | `payload launched successfully` then silence, no log | Loader segfault (see above) | Update GoldHEN โ‰ฅ v2.4b18.5, use BinLoader server | | `Error handling payload` | Loader rejected the bytes | Re-check file integrity (`shasum`), resend | | Log exists but `FATAL: token rejected (4004)` | Token rotated/dead | Fresh token into `/data/orbisRPC/config.json`, relaunch | diff --git a/installer/Makefile b/installer/Makefile index 5dbc3ac..189aeb4 100644 --- a/installer/Makefile +++ b/installer/Makefile @@ -68,8 +68,8 @@ $(INTDIR)/jsonlite.o: orbisrpc/jsonlite.c $(CC) $(CFLAGS) -o $@ $< # Stage the daemon + evict payloads + config into the app image. -# config.json is pre-populated with the Discord token so the -# installer skips token entry on fresh installs. +# config.json carries the SET_ME placeholder; a pre-seeded valid token +# (kept out of the repo) makes the installer skip token entry. installer/assets/daemon.elf: build-sdk/orbisrpc_sdk.elf cp $< $@ installer/assets/evict.elf: build-sdk/evict.elf diff --git a/installer/installer.c b/installer/installer.c index f8bccd9..52bb811 100644 --- a/installer/installer.c +++ b/installer/installer.c @@ -242,8 +242,8 @@ static int step_files(void){ FILE *src = fopen("/app0/assets/config.json", "rb"); if(src){ fclose(src); - copy_file("/app0/assets/config.json", ICFG_PATH); - ilog("cfg-copy", 0, 0, 0); + int rc = copy_file("/app0/assets/config.json", ICFG_PATH); + ilog("cfg-copy", rc == 0 ? 0 : (errno ? errno : -1), 0, 0); } else { ilog("cfg-noasset", errno, 0, 0); /* Fallback: write template with pre-set token. */ diff --git a/installer/nettest.c b/installer/nettest.c deleted file mode 100644 index 1370b88..0000000 --- a/installer/nettest.c +++ /dev/null @@ -1,43 +0,0 @@ -/* nettest.c - connect-only probes. A closed port and a filtered host - * both read as unreachable; that honesty is the point (Sony TMDB from - * the console genuinely fails while Discord succeeds). */ -#include "nettest.h" -#include "send.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -/* Non-blocking connect with a hard deadline lives in send.c (shared); - * blocking connect ignores SNDTIMEO on filtered hosts (75 s hole). */ - -int net_probe(const char *host, int port, int timeout_s){ - struct addrinfo hints, *res = NULL, *rp; - char svc[8]; - int ok = 0; - if(!host || !host[0] || port <= 0 || port > 65535) return 0; - if(net_init() != 0) return 0; /* stack dead: everything unreachable */ - if(timeout_s < 1) timeout_s = 1; - if(timeout_s > 10) timeout_s = 10; - snprintf(svc, sizeof svc, "%d", port); - memset(&hints, 0, sizeof hints); - hints.ai_family = AF_INET; /* v4 only: deterministic on console stacks */ - hints.ai_socktype = SOCK_STREAM; - if(getaddrinfo(host, svc, &hints, &res) != 0 || !res) return 0; - for(rp = res; rp; rp = rp->ai_next){ - int fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); - if(fd < 0) continue; - if(sock_connect_deadline(fd, rp->ai_addr, rp->ai_addrlen, timeout_s) == 0) ok = 1; - close(fd); - if(ok) break; - } - freeaddrinfo(res); - return ok; -} diff --git a/installer/nettest.h b/installer/nettest.h deleted file mode 100644 index 32054a5..0000000 --- a/installer/nettest.h +++ /dev/null @@ -1,8 +0,0 @@ -/* nettest.h - honest TCP reachability probes (no traffic beyond connect). */ -#ifndef INSTALLER_NETTEST_H -#define INSTALLER_NETTEST_H - -/* 1 reachable, 0 not. timeout_s clamped 1..10. Host-testable. */ -int net_probe(const char *host, int port, int timeout_s); - -#endif diff --git a/installer/send.c b/installer/send.c deleted file mode 100644 index ce97c0b..0000000 --- a/installer/send.c +++ /dev/null @@ -1,136 +0,0 @@ -/* send.c - loopback payload injection: file bytes over TCP to the - * console's own loaders. Chunked send (no whole-file malloc). */ -#include "send.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef INSTALLER_PS4 -#include -#include -#endif - -#define SEND_CHUNK (64u*1024u) -static const int SEND_PORTS[] = { 9090, 9021, 9020 }; - -int net_init(void){ -#ifdef INSTALLER_PS4 - static int done = 0; - int probe; - if(done) return 0; - /* Internal NET module (Payload Guest boot pattern) + best-effort - * stack init. Ground truth is a probe socket, not return codes. */ - (void)sceSysmoduleLoadModuleInternal(ORBIS_SYSMODULE_INTERNAL_NET); - (void)sceNetInit(); - (void)sceNetPoolCreate("orbisrpc", 64*1024, 0); - probe = socket(AF_INET, SOCK_STREAM, 0); - if(probe < 0) return -1; - close(probe); - done = 1; -#endif - return 0; -} - -int sock_connect_deadline(int fd, const struct sockaddr *sa, socklen_t len, int timeout_s){ - int flags, rc, err = 0; - socklen_t elen = sizeof err; - fd_set wf; - struct timeval tv; - if(timeout_s < 1) timeout_s = 1; - if(timeout_s > 15) timeout_s = 15; - flags = fcntl(fd, F_GETFL, 0); - if(flags < 0) return -1; - if(fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) return -1; - rc = connect(fd, sa, len); - if(rc == 0){ - fcntl(fd, F_SETFL, flags); - return 0; - } - if(errno != EINPROGRESS){ - fcntl(fd, F_SETFL, flags); - return -1; - } - FD_ZERO(&wf); - FD_SET(fd, &wf); - tv.tv_sec = timeout_s; - tv.tv_usec = 0; - rc = select(fd + 1, NULL, &wf, NULL, &tv); - fcntl(fd, F_SETFL, flags); - if(rc <= 0) return -1; - if(getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &elen) != 0) return -1; - return err == 0 ? 0 : -1; -} - -static int send_one(int fd, const char *path, void (*progress)(unsigned)){ - FILE *f = fopen(path, "rb"); - /* Static, not stack: 64 KB is a real fraction of an app thread stack. */ - static unsigned char buf[SEND_CHUNK]; - long total = 0, sent = 0; - size_t n; - if(!f) return -1; - if(fseek(f, 0, SEEK_END) != 0){ fclose(f); return -1; } - total = ftell(f); - if(total <= 0 || total > 16*1024*1024){ fclose(f); return -1; } - if(fseek(f, 0, SEEK_SET) != 0){ fclose(f); return -1; } - if(progress) progress(0); - while((n = fread(buf, 1, sizeof buf, f)) > 0){ - size_t off = 0; - int stalls = 0; - while(off < n){ - ssize_t w = send(fd, buf + off, n - off, 0); - if(w <= 0){ - /* Blocking socket + SNDTIMEO: EAGAIN means the loader - * stopped reading. Retry briefly, then fail instead of - * spinning forever. */ - if((errno == EAGAIN || errno == EWOULDBLOCK) && ++stalls < 4) - continue; - fclose(f); - return -1; - } - stalls = 0; - off += (size_t)w; - } - sent += (long)n; - if(progress) progress((unsigned)(sent * 100 / total)); - } - fclose(f); - if(progress) progress(100); - return 0; -} - -int send_file_loopback(const char *path, int *port_used, void (*progress)(unsigned)){ - unsigned i; - if(!path || !path[0]) return -1; - if(net_init() != 0) return -1; /* no stack, no sockets: fail, don't crash */ - for(i = 0; i < sizeof SEND_PORTS/sizeof SEND_PORTS[0]; i++){ - int fd = socket(AF_INET, SOCK_STREAM, 0); - struct sockaddr_in sa; - struct timeval tv = { 20, 0 }; - if(fd < 0) continue; - memset(&sa, 0, sizeof sa); - sa.sin_family = AF_INET; - sa.sin_port = htons((uint16_t)SEND_PORTS[i]); - sa.sin_addr.s_addr = inet_addr("127.0.0.1"); - if(sock_connect_deadline(fd, (struct sockaddr *)&sa, sizeof sa, 5) < 0){ - close(fd); - continue; - } - /* Back to blocking for the bulk send, WITH a send timeout so a - * stalled loader surfaces EAGAIN (capped retries below) instead - * of wedging forever. */ - setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); - if(send_one(fd, path, progress) == 0){ - close(fd); - if(port_used) *port_used = SEND_PORTS[i]; - return 0; - } - close(fd); - } - return -1; -} diff --git a/installer/send.h b/installer/send.h deleted file mode 100644 index df95af7..0000000 --- a/installer/send.h +++ /dev/null @@ -1,19 +0,0 @@ -/* send.h - loopback payload injection (clean-room, standard sockets). - * Tries native GoldHEN BinLoader 9090, then elfldr 9021, then 9020. - * 9020 is one-shot: accepted bytes count as sent. */ -#ifndef INSTALLER_SEND_H -#define INSTALLER_SEND_H -#include - -/* progress(pct) may be NULL. Returns 0 + port_used set on success. */ -int send_file_loopback(const char *path, int *port_used, void (*progress)(unsigned pct)); - -/* Non-blocking connect with a hard deadline (filtered hosts can't stall - * past timeout_s). 0 connected, -1 failed. Leaves fd blocking. */ -int sock_connect_deadline(int fd, const struct sockaddr *sa, socklen_t len, int timeout_s); - -/* One-time network stack init (sceNetInit + pool). 0 ready, -1 dead. - * No-op returning 0 on host test builds. Safe to call repeatedly. */ -int net_init(void); - -#endif diff --git a/installer/ui.c b/installer/ui.c index d612c2d..c43a35b 100644 --- a/installer/ui.c +++ b/installer/ui.c @@ -113,39 +113,6 @@ int ui_ok(const char *msg){ return 0; } -/* Yes/No dialog. Returns 1 (Yes) or 0 (No/closed). - * The dialog uses YESNO_FOCUS_NO which focuses the "No" button. - * On this console, Circle=confirm and X=back. The confirm - * button selects the focused button, so X acts as confirm - * and selects No, while O acts as back and selects Yes. - * Since the dialog inverts the result, this returns 1 when - * X is pressed (Yes) and 0 when O is pressed (No) โ€” matching - * the system's confirm button behavior. Kept for future use. */ -int ui_confirm(const char *msg){ - OrbisMsgDialogParam param; - OrbisMsgDialogUserMessageParam um; - OrbisMsgDialogResult res; - memset(&res, 0, sizeof res); - sceMsgDialogTerminate(); - if(sceMsgDialogInitialize() < 0) return -1; - base_init(¶m); - memset(&um, 0, sizeof um); - um.msg = msg; - /* YESNO_FOCUS_NO focuses the "No" button. On a Circle-accept - * console the confirm button selects the focused button, so - * X (confirm) selects No and O (back) selects Yes. The - * result is inverted: Xโ†’buttonId=NOโ†’returns 1 (Yes). */ - um.buttonType = ORBIS_MSG_DIALOG_BUTTON_TYPE_YESNO_FOCUS_NO; - param.userMsgParam = &um; - if(sceMsgDialogOpen(¶m) < 0){ sceMsgDialogTerminate(); return -1; } - while(sceMsgDialogUpdateStatus() != ORBIS_COMMON_DIALOG_STATUS_FINISHED) - sceKernelUsleep(20000); - sceMsgDialogClose(); - sceMsgDialogGetResult(&res); - sceMsgDialogTerminate(); - return res.buttonId == ORBIS_MSG_DIALOG_BUTTON_ID_YES; -} - static int progress_open = 0; int ui_progress_open(const char *msg){ diff --git a/installer/ui.h b/installer/ui.h index efa051d..39a45a7 100644 --- a/installer/ui.h +++ b/installer/ui.h @@ -10,10 +10,6 @@ int ui_init(void); /* Info dialog with OK. 0 shown, -1 failed to open. */ int ui_ok(const char *msg); -/* Yes/No dialog: X = enter (Yes), O = back (No). 1 yes, 0 no/closed, - * -1 error. */ -int ui_confirm(const char *msg); - /* Progress dialog. Open once, update, close. 0 ok, -1 error. */ int ui_progress_open(const char *msg); void ui_progress_msg(const char *msg); diff --git a/orbisrpc/daemon.c b/orbisrpc/daemon.c index 7c7af8b..c8530e6 100644 --- a/orbisrpc/daemon.c +++ b/orbisrpc/daemon.c @@ -83,6 +83,8 @@ static void sess_save(const char *tid, const char *name, int64_t started){ jl FILE *f = fopen("/data/orbisRPC/session.json.new", "wb"); if(f){ int ok = (fputs(s, f) >= 0) && (fflush(f) == 0); + /* force bytes to disk before rename (same as cfg_save) */ + if(ok){ int fd = fileno(f); if(fd < 0 || fsync(fd) != 0) ok = 0; } if(fclose(f) != 0) ok = 0; if(ok) rename("/data/orbisRPC/session.json.new", "/data/orbisRPC/session.json"); diff --git a/orbisrpc/detect.c b/orbisrpc/detect.c index 9e3648b..bbf874a 100644 --- a/orbisrpc/detect.c +++ b/orbisrpc/detect.c @@ -573,7 +573,7 @@ int detect_name_for_title(const char *titleId, char *out_name, size_t cap){ /* Game-process-safe only: small reads plus one bounded network * lookup; no multi-megabyte scans anywhere in this codebase. */ if(cfg_title(&g_cfg, titleId, out_name, cap)==0){ log_msg("name: %s via config", out_name); resolve_remember(titleId, out_name, "", 1); return 0; } - /* appdb_title(titleId, out_name, cap); */ resolve_remember(titleId, out_name, "", 1); return 0; + /* appdb_title removed: no baked table (README "tables-none"). */ if(pronunc_title(titleId, out_name, cap)==0){ log_msg("name: %s via appmeta", out_name); resolve_remember(titleId, out_name, "", 1); return 0; } else log_msg("name: appmeta miss for %s", titleId); if(sfo_file_title(titleId, out_name, cap)==0){ log_msg("name: %s via sfo", out_name); resolve_remember(titleId, out_name, "", 1); return 0; } diff --git a/orbisrpc/release_pubkey.h b/orbisrpc/release_pubkey.h index a03665e..ddac819 100644 --- a/orbisrpc/release_pubkey.h +++ b/orbisrpc/release_pubkey.h @@ -1,22 +1,22 @@ /* release_pubkey.h - embedded release-channel public key (P-256, raw X||Y). * - * DEV KEY: generated for v1.0.1 development. Before publishing a real - * release, generate a production keypair OFFLINE, replace these bytes, - * and keep the private key out of the repo: + * PRODUCTION KEY: generated 2026-09-25 with * openssl ecparam -name prime256v1 -genkey -noout -out release_priv.pem - * openssl ec -in release_priv.pem -pubout -out release_pub.pem - * Sign manifest.json with scripts/make_manifest.py (or openssl dgst). + * The private key lives OFFLINE outside the repo (macOS host: + * ~/.config/orbisrpc/release_priv.pem) and must be backed up offline: + * losing it bricks the on-console updater; leaking it fakes releases. + * Sign manifest.json with scripts/make_manifest.py --priv release_priv.pem. */ #ifndef ORBISRPC_RELEASE_PUBKEY_H #define ORBISRPC_RELEASE_PUBKEY_H /* Uncompressed P-256 X || Y (64 bytes). */ static const unsigned char ORBISRPC_RELEASE_PUBKEY[64] = { - 0x47,0xb8,0x9d,0xb8,0x85,0x3d,0x6c,0xcf,0x7e,0x1a,0xbe,0x6e,0xd1,0x5c,0x2e,0x69, - 0x66,0x2f,0xa8,0x09,0xbf,0x6d,0xf8,0x9c,0x50,0xe4,0x10,0x0b,0x79,0x4b,0x66,0x1a, - 0xd7,0x79,0xae,0x2e,0x41,0xc9,0x35,0x5c,0x6f,0xe3,0xba,0xed,0xc9,0x93,0x9a,0xca, - 0xe8,0x18,0xd5,0x4b,0xeb,0x84,0x20,0x8d,0xb3,0xd2,0x4a,0xfc,0x65,0x81,0x6c,0x6f + 0xe7,0x31,0xa1,0x93,0x9f,0xe3,0x85,0x36,0x03,0xbf,0x3e,0xb1,0xf0,0xc0,0x55,0x80, + 0x4e,0xa9,0x19,0xc5,0xca,0xe5,0xe8,0x5c,0x36,0xdc,0x0d,0x6f,0x7e,0x46,0x03,0xdb, + 0x23,0x2b,0xb9,0x2e,0xb1,0x72,0xa8,0xc4,0x75,0x15,0x99,0x18,0x58,0x59,0xfe,0x8c, + 0x4d,0x4f,0x31,0xee,0xa3,0xdd,0xfb,0x15,0xae,0x09,0xc3,0x34,0xee,0xf9,0x16,0x54 }; -#define ORBISRPC_RELEASE_KEY_ID "dev-2026-09-20" +#define ORBISRPC_RELEASE_KEY_ID "prod-2026-09-25" #endif diff --git a/orbisrpc/tmdb.c b/orbisrpc/tmdb.c index 74aa145..823cf1d 100644 --- a/orbisrpc/tmdb.c +++ b/orbisrpc/tmdb.c @@ -281,20 +281,19 @@ int tmdb_resolve(const char *titleId, char *name, size_t name_cap, return name[0] ? 0 : -1; } } - /* Live Sony CDN first (TMDB over TLS; plain HTTP is blocked - * on-console and handled inside http_get/https fallback below). - * No baked tables: CUSA code + Sony CDN is the source of truth. */ + /* Live Sony CDN over TLS first (plain HTTP is blocked on jailbroken + * consoles โ€” http_get below is last-resort only). No baked tables: + * CUSA code + Sony CDN is the source of truth. */ char path[128]; if(tmdb_path(titleId, path, sizeof path) != 0) return -1; static char body[TMDB_BODY_MAX]; int status = 0; - int n = http_get(TMDB_HOST, path, body, sizeof body, &status); + int n = https_get_tmdb(path, body, sizeof body, &status); + if(n > 0) log_msg("tmdb: live via https for %s", titleId); if(n <= 0){ - /* Port 80 is blocked from jailbroken consoles; Sony also answers - * the same paths over TLS (443). Try HTTPS before giving up so - * new installs resolve live like everything else. */ - n = https_get_tmdb(path, body, sizeof body, &status); - if(n > 0) log_msg("tmdb: live via https for %s", titleId); + /* Plain HTTP (port 80): blocked from jailbroken consoles, so this + * burns the connect timeout before giving up โ€” keep it last. */ + n = http_get(TMDB_HOST, path, body, sizeof body, &status); } if(n <= 0){ log_msg("tmdb: fetch fail status=%d", status); return -1; } char iname[128] = "", iicon[256] = ""; diff --git a/orbisrpc/updater.c b/orbisrpc/updater.c index 6684686..0e28574 100644 --- a/orbisrpc/updater.c +++ b/orbisrpc/updater.c @@ -172,7 +172,7 @@ static char *https_get_once(const char *host, const char *path, if(tls_write(t, req, (size_t)rl) < 0){ tls_free(t); return NULL; } /* Read raw response (headers + body) up to header cap + body cap. */ size_t rawcap = UPD_HDR_MAX + cap; - char *raw = (char*)malloc(rawcap); + char *raw = (char*)malloc(rawcap + 1); /* +1: NUL pad when a read fills cap exactly */ if(!raw){ tls_free(t); return NULL; } size_t rl2 = 0; int64_t dl = orbis_mono_s() + UPD_DEADLINE_S + 20; @@ -280,57 +280,6 @@ static int stage_file(const char *target, const unsigned char *data, size_t n){ return 0; } -/* SHA256 of a buffer, hex-encoded (64 chars + NUL). Returns 0 on success. */ -static int sha256_hex(const unsigned char *data, size_t n, char out[65]){ - unsigned char dig[32]; - mbedtls_sha256_context sc; - mbedtls_sha256_init(&sc); - int ok = mbedtls_sha256_starts(&sc, 0) == 0 && - mbedtls_sha256_update(&sc, data ? data : (const unsigned char *)"", n) == 0 && - mbedtls_sha256_finish(&sc, dig) == 0; - mbedtls_sha256_free(&sc); - if(!ok) return -1; - for(int i=0;i<32;i++) snprintf(out+2*i, 3, "%02x", dig[i]); - out[64]=0; - return 0; -} - -/* Look up "" in a SHA256SUMS body (" \n" lines). - * Returns 0 and fills want_hex when found. */ -static int sums_lookup(const char *sums, const char *filename, char want_hex[65]){ - size_t fnlen = strlen(filename); - const char *p = sums; - while(*p){ - while(*p==' '||*p=='\t'||*p=='\r'||*p=='\n') p++; - if(!*p) break; - if(strlen(p) < 64) break; - char hex[65]; - memcpy(hex, p, 64); hex[64]=0; - int ishex=1; - for(int i=0;i<64;i++){ - char c=hex[i]; - if(!((c>='0'&&c<='9')||(c>='a'&&c<='f')||(c>='A'&&c<='F'))){ ishex=0; break; } - } - const char *e = strchr(p, '\n'); - size_t linelen = e ? (size_t)(e-p) : strlen(p); - if(ishex && linelen > 65){ - const char *nl = p+64; - while(nl= fnlen && !memcmp(nl, filename, fnlen) && - (nl[fnlen]=='\n'||nl[fnlen]=='\r'||nl[fnlen]==' '||nl[fnlen]=='\t'||nl[fnlen]==0||nl[fnlen]=='*')){ - for(int i=0;i<64;i++){ - char c=hex[i]; - want_hex[i]=(c>='A'&&c<='F')?(char)(c-'A'+'a'):c; - } - want_hex[64]=0; - return 0; - } - } - p = e ? e+1 : p+linelen; - } - return -1; -} - /* Download a release asset by exact name. Returns heap body or NULL. */ static char *fetch_asset(const jl_val_t *assets, const char *want_name, size_t cap, int *status, size_t *out_len){ @@ -428,12 +377,14 @@ int updater_check_and_stage(void){ free(sbody); if(!have_manifest){ free(mbody); mbody = NULL; } } - /* Compat fallback: SHA256SUMS (unsigned but hash-pinned). */ - char *sums = NULL; + /* Refuse unsigned updates outright. SHA256SUMS comes from the + * same release as the binaries, so it pins nothing against + * release-asset compromise โ€” exactly what the signed manifest + * defends against (ORX-UPDATE-002). */ if(!have_manifest){ - sums = fetch_asset(assets, "SHA256SUMS", 65536, &status, NULL); - if(!sums) - log_msg("updater: WARN ORX-UPDATE-002: no manifest and no SHA256SUMS; refusing unsigned update"); + log_msg("updater: no valid signed manifest; refusing update (ORX-UPDATE-002)"); + jl_free(r); + return 0; } /* Two-phase commit: download + verify EVERY asset first, then * activate all at once. A failure anywhere stages nothing, so @@ -468,31 +419,8 @@ int updater_check_and_stage(void){ pend_fail = 1; break; } - if(have_manifest){ - if(manifest_check(&mf, nm->str, (unsigned char*)bin, al) != 0){ - log_msg("updater: asset %s not in manifest or hash mismatch; refusing (ORX-UPDATE-002)", nm->str); - free(bin); - pend_fail = 1; - break; - } - } else if(sums){ - char want[65]; - if(sums_lookup(sums, nm->str, want) != 0){ - log_msg("updater: asset %s not in SHA256SUMS; refusing", nm->str); - free(bin); - pend_fail = 1; - break; - } - char got[65]; - if(sha256_hex((unsigned char*)bin, al, got) != 0 || strcmp(got, want) != 0){ - log_msg("updater: asset %s hash mismatch; refusing", nm->str); - free(bin); - pend_fail = 1; - break; - } - } else { - /* No manifest and no SHA256SUMS: refuse unsigned bytes. */ - log_msg("updater: asset %s refused: no trust anchor (ORX-UPDATE-002)", nm->str); + if(manifest_check(&mf, nm->str, (unsigned char*)bin, al) != 0){ + log_msg("updater: asset %s not in manifest or hash mismatch; refusing (ORX-UPDATE-002)", nm->str); free(bin); pend_fail = 1; break; @@ -519,7 +447,6 @@ int updater_check_and_stage(void){ log_msg("updater: incomplete set; staged nothing (versions stay matched)"); } for(int pi = 0; pi < pend_n; pi++) free(pend[pi].bin); - free(sums); free(mbody); } } diff --git a/tests/Makefile b/tests/Makefile index 4bdb7e4..f8732d6 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -9,7 +9,7 @@ MBEDTLS_DIR := ../third_party/mbedtls/library MBEDTLS_SRCS := $(filter-out $(MBEDTLS_DIR)/net_sockets.c $(MBEDTLS_DIR)/timing.c $(MBEDTLS_DIR)/entropy_poll.c,$(wildcard $(MBEDTLS_DIR)/*.c)) ORBIS_SRCS := ../orbisrpc/jsonlite.c ../orbisrpc/b64.c ../orbisrpc/sfo.c ../orbisrpc/tmdb_crypto.c ../orbisrpc/updater_util.c ../orbisrpc/art.c ../orbisrpc/log.c ../orbisrpc/health.c ../orbisrpc/manifest.c ../orbisrpc/compat.c ../orbisrpc/cfg.c ../orbisrpc/appdb.c ../orbisrpc/discord.c -INST_SRCS := ../installer/icfg.c ../installer/nettest.c ../installer/send.c +INST_SRCS := ../installer/icfg.c # SQLite amalgamation: -O0 for host iteration speed (payload uses -O2). SQLITE_DIR := ../third_party/sqlite/sqlite-amalgamation-3510100 SQLITE_FLAGS := -O0 -DSQLITE_THREADSAFE=0 -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_OMIT_DEPRECATED -I$(SQLITE_DIR) diff --git a/tests/e2e_consumer.py b/tests/e2e_consumer.py index 1d3db17..86ce0e0 100755 --- a/tests/e2e_consumer.py +++ b/tests/e2e_consumer.py @@ -34,6 +34,28 @@ check("host/unit-tests", r.returncode == 0 and "utility tests passed" in r.stdou cfg = src("config/config.json") check("install/template-has-no-token", "SET_ME" in cfg) check("install/art-pack-default", "orbisrpc-host" in cfg) +# 2b. no Discord-session-shaped token anywhere the PKG or repo ships. +# Fatal when ORBISRPC_STRICT_SECRETS=1 (CI); a local working copy may hold +# a dev token and gets a warning instead. +tok_re = re.compile(rb"MT[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{20,}") +leaks = [] +for rel in ("installer/installer.c", "installer/config.json", + "installer/assets/config.json", "config/config.json"): + try: + with open(os.path.join(ROOT, rel), "rb") as fh: + if tok_re.search(fh.read()): + leaks.append(rel) + except OSError: + pass +if leaks: + _msg = "token-shaped string in " + ", ".join(leaks) + if os.environ.get("ORBISRPC_STRICT_SECRETS") == "1": + check("secrets/no-token-in-installer", False, _msg) + else: + print("WARN secrets/no-token-in-installer (set ORBISRPC_STRICT_SECRETS=1 " + "to fail) - " + _msg) +else: + check("secrets/no-token-in-installer", True) # 3. single-instance lock with recycled-PID guard lock = src("orbisrpc/lock.c") check("runtime/single-lock", "lock_acquire" in src("orbisrpc/daemon.c")) diff --git a/tests/test_utils.c b/tests/test_utils.c index b0f5771..a63722f 100644 --- a/tests/test_utils.c +++ b/tests/test_utils.c @@ -11,7 +11,6 @@ #include "../orbisrpc/discord.h" #include "../orbisrpc/detect.h" #include "../installer/icfg.h" -#include "../installer/nettest.h" #include "sqlite3.h" #include @@ -611,11 +610,6 @@ static void test_installer_cfg(void) { assert(icfg_get_int(path, "poll_interval_s", &n) == 0 && n == 12); /* missing file: loads fail soft, save still works */ assert(icfg_get_str("/nonexistent/x.json", "k", st, sizeof st) != 0); - /* net probes fail soft on garbage */ - assert(net_probe(NULL, 443, 2) == 0); - assert(net_probe("", 443, 2) == 0); - assert(net_probe("127.0.0.1", 1, 1) == 0); - assert(net_probe("nonexistent.invalid", 443, 1) == 0); } int main(void) { diff --git a/tests/test_utils_asan b/tests/test_utils_asan deleted file mode 100755 index 64eb67d..0000000 Binary files a/tests/test_utils_asan and /dev/null differ