mirror of
https://github.com/jmelovich/Steam-Link-PS5.git
synced 2026-10-19 02:10:53 -05:00
M1: pairing and streaming request succeed with Steam's PIN-masked key exchange
Current Steam rejects the legacy key-escrow pairing once the PIN is typed. The ihslib submodule now points at scaronni's 'auth' branch (upstream master + X25519/ SHA-256 key exchange, f68de5f0), pending its merge into mariotaku/ihslib. The key negotiated at pairing is per host: app_storage keeps it as host-<clientid>.key and the client restores it before each streaming request. tools/relaunch.sh and tools/stop.sh make the console loop one command; the flow backs off 60 s after a failed pairing instead of re-prompting the PC every 5 s; a crypto self-test logs at startup. Tested on the console: PIN accepted, stream granted, session authenticated.
This commit is contained in:
1 parent
d3049b4bc6
commit
670fa869ac
7 files changed
+131
-4
No files matched your search
+2
-1
@@ -1,6 +1,7 @@
|
||||
[submodule "third_party/ihslib"]
|
||||
path = third_party/ihslib
|
||||
url = https://github.com/mariotaku/ihslib.git
|
||||
url = https://github.com/scaronni/ihslib.git
|
||||
branch = auth
|
||||
[submodule "third_party/protobuf-c"]
|
||||
path = third_party/protobuf-c
|
||||
url = https://github.com/protobuf-c/protobuf-c.git
|
||||
|
||||
@@ -106,6 +106,42 @@ const Identity &identity()
|
||||
return g_identity;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
std::string host_key_path(std::uint64_t host_client_id)
|
||||
{
|
||||
char name[64];
|
||||
std::snprintf(name, sizeof name, "/host-%016llx.key", static_cast<unsigned long long>(host_client_id));
|
||||
return g_data_dir + name;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool host_key_load(std::uint64_t host_client_id, std::uint8_t key[32])
|
||||
{
|
||||
if (g_data_dir.empty())
|
||||
return false;
|
||||
const int fd = open(host_key_path(host_client_id).c_str(), O_RDONLY);
|
||||
if (fd < 0)
|
||||
return false;
|
||||
const ssize_t got = read(fd, key, 32);
|
||||
close(fd);
|
||||
return got == 32;
|
||||
}
|
||||
|
||||
void host_key_save(std::uint64_t host_client_id, const std::uint8_t key[32])
|
||||
{
|
||||
if (g_data_dir.empty())
|
||||
return;
|
||||
const int fd = open(host_key_path(host_client_id).c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
|
||||
if (fd < 0)
|
||||
{
|
||||
log("host key could not be saved: %s", std::strerror(errno));
|
||||
return;
|
||||
}
|
||||
write(fd, key, 32);
|
||||
close(fd);
|
||||
}
|
||||
|
||||
void random_bytes(void *destination, std::size_t length)
|
||||
{
|
||||
if (ps5_get_random(destination, length) != 0)
|
||||
|
||||
@@ -26,4 +26,9 @@ const char *data_dir();
|
||||
const Identity &identity();
|
||||
// Console entropy (sysctl kern.rng_pseudo). Not for keys that leave the console.
|
||||
void random_bytes(void *destination, std::size_t length);
|
||||
|
||||
// Pairing negotiates a 32-byte secret per host (Steam's PIN-masked key exchange);
|
||||
// streaming from that host needs the same key again. Keyed by the host's client id.
|
||||
bool host_key_load(std::uint64_t host_client_id, std::uint8_t key[32]);
|
||||
void host_key_save(std::uint64_t host_client_id, const std::uint8_t key[32]);
|
||||
} // namespace slp
|
||||
+39
-2
@@ -105,6 +105,28 @@ void ihs_log(IHS_LogLevel level, const char *tag, const char *message)
|
||||
slp::log("[IHS.%s %s] %s", tag, IHS_LogLevelName(level), message);
|
||||
}
|
||||
|
||||
// ---- crypto self-test: the pairing ticket is RSA-OAEP encrypted; a silent failure
|
||||
// there makes Steam reject every PIN. ihslib does not export these; the archive does.
|
||||
extern "C" int IHS_CryptoRSAEncrypt(const std::uint8_t *in, std::size_t in_length, const std::uint8_t *key,
|
||||
std::size_t key_length, std::uint8_t *out, std::size_t *out_length);
|
||||
extern "C" bool IHS_ClientAuthorizationPubKey(IHS_Client *client, IHS_SteamUniverse universe, std::uint8_t *key,
|
||||
std::size_t *key_length);
|
||||
extern "C" std::uint32_t IHS_CryptoRandomUInt32();
|
||||
|
||||
void crypto_self_test(IHS_Client *client)
|
||||
{
|
||||
std::uint8_t key[384];
|
||||
std::size_t key_length = sizeof key;
|
||||
const bool have_key = IHS_ClientAuthorizationPubKey(client, IHS_SteamUniversePublic, key, &key_length);
|
||||
std::uint8_t out[2048];
|
||||
std::size_t out_length = sizeof out;
|
||||
const std::uint8_t plain[] = "self-test";
|
||||
const int result = IHS_CryptoRSAEncrypt(plain, sizeof plain, key, key_length, out, &out_length);
|
||||
slp::log("crypto self-test: pubkey %s (%zu bytes), rsa encrypt result %d (-0x%x), %zu bytes out, random %u",
|
||||
have_key ? "ok" : "MISSING", key_length, result, static_cast<unsigned>(-result), out_length,
|
||||
IHS_CryptoRandomUInt32());
|
||||
}
|
||||
|
||||
// ---- client phase: discovery, authorization, streaming request -------------
|
||||
|
||||
struct ClientFlow
|
||||
@@ -116,6 +138,7 @@ struct ClientFlow
|
||||
bool have_session = false;
|
||||
IHS_SessionInfo session{};
|
||||
char pin[8] = {};
|
||||
std::uint8_t host_key[32] = {};
|
||||
};
|
||||
|
||||
void request_stream(IHS_Client *client, ClientFlow &flow)
|
||||
@@ -151,6 +174,12 @@ void on_discovered(IHS_Client *client, const IHS_HostInfo *host, void *context)
|
||||
status.host = std::string{host->hostname} + " " + address;
|
||||
}
|
||||
free(address);
|
||||
// Already paired with this PC: the stream request must use the key negotiated then.
|
||||
if (slp::host_key_load(host->clientId, flow.host_key))
|
||||
{
|
||||
IHS_ClientSetSecretKey(client, flow.host_key);
|
||||
slp::log("using the stored key for %s", host->hostname);
|
||||
}
|
||||
request_stream(client, flow);
|
||||
}
|
||||
|
||||
@@ -163,6 +192,9 @@ void on_authorization_success(IHS_Client *client, const IHS_HostInfo *, std::uin
|
||||
{
|
||||
auto &flow = *static_cast<ClientFlow *>(context);
|
||||
slp::log("authorized, steam id %llu", static_cast<unsigned long long>(steam_id));
|
||||
// The client now holds the key negotiated with this host; keep it for next time.
|
||||
IHS_ClientGetSecretKey(client, flow.host_key);
|
||||
slp::host_key_save(flow.host.clientId, flow.host_key);
|
||||
{
|
||||
std::lock_guard lock{status.mutex};
|
||||
status.pin.clear();
|
||||
@@ -307,9 +339,11 @@ void *flow_thread(void *)
|
||||
{
|
||||
flow.host_chosen = false;
|
||||
flow.have_session = false;
|
||||
flow.authorization_tried = false;
|
||||
set_state("LOOKING FOR A PC RUNNING STEAM");
|
||||
IHS_Client *client = IHS_ClientCreate(&flow.config);
|
||||
IHS_ClientSetLogFunction(client, ihs_log);
|
||||
crypto_self_test(client);
|
||||
IHS_ClientDiscoveryCallbacks discovery{};
|
||||
discovery.discovered = on_discovered;
|
||||
IHS_ClientAuthorizationCallbacks authorization{};
|
||||
@@ -330,7 +364,9 @@ void *flow_thread(void *)
|
||||
|
||||
if (flow.have_session)
|
||||
{
|
||||
IHS_Session *session = IHS_SessionCreate(&flow.config, &flow.session);
|
||||
IHS_ClientConfig session_config = flow.config;
|
||||
session_config.secretKey = flow.host_key;
|
||||
IHS_Session *session = IHS_SessionCreate(&session_config, &flow.session);
|
||||
IHS_SessionSetLogFunction(session, ihs_log);
|
||||
IHS_StreamSessionCallbacks session_callbacks{};
|
||||
session_callbacks.configuring = on_session_configuring;
|
||||
@@ -357,7 +393,8 @@ void *flow_thread(void *)
|
||||
slp::log("IHS_SessionConnect failed");
|
||||
IHS_SessionDestroy(session);
|
||||
}
|
||||
sleep(5);
|
||||
// A failed pairing re-prompts the PC on every retry: back off properly.
|
||||
sleep(flow.authorization_tried && !flow.have_session ? 60 : 5);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Vendored
+1
-1
Submodule third_party/ihslib updated: 1881b9a05f...f68de5f07e.
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
# steamlink-ps5 - The edit-deploy-test loop in one command.
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# PS5_HOST=192.168.x.x tools/relaunch.sh [--no-build]
|
||||
#
|
||||
# Stops the title if it is running (a running title's files cannot be replaced),
|
||||
# uploads dist/<TITLE_ID> over FTP, launches the title through shsrv and prints the
|
||||
# first lines of its log. Needs ftpsrv (2121) and shsrv (2323) on the console.
|
||||
set -euo pipefail
|
||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
host=${PS5_HOST:?set PS5_HOST to the console IP address}
|
||||
title_id=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["titleId"])' "$root/sce_sys/param.json")
|
||||
sh() { timeout 30 python3 "$root/tools/ps5sh.py" "$host" "$@"; }
|
||||
|
||||
[[ ${1:-} == --no-build ]] || make -C "$root" --no-print-directory app
|
||||
pid=$(sh ps 2>/dev/null | awk -v t="$title_id" '$8 == t {print $1}' || true)
|
||||
if [[ -n $pid ]]; then
|
||||
echo "==> [relaunch] stopping $title_id (pid $pid)"
|
||||
sh "kill $pid" >/dev/null 2>&1 || true
|
||||
sleep 4
|
||||
fi
|
||||
PS5_HOST=$host bash "$root/tools/deploy.sh" | tail -1
|
||||
echo "==> [relaunch] launching $title_id"
|
||||
sh "launch $title_id" >/dev/null 2>&1 || true
|
||||
sleep "${RELAUNCH_WAIT:-12}"
|
||||
echo "==> [relaunch] log:"
|
||||
curl -s --max-time 10 "ftp://$host:2121/data/steamlink-ps5/log.txt" | tail -n "${RELAUNCH_LINES:-25}"
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
# steamlink-ps5 - Stop the title on the console (shsrv), if it is running.
|
||||
# PS5_HOST=192.168.x.x tools/stop.sh
|
||||
set -euo pipefail
|
||||
root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
|
||||
host=${PS5_HOST:?set PS5_HOST to the console IP address}
|
||||
title_id=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["titleId"])' "$root/sce_sys/param.json")
|
||||
pid=$(timeout 30 python3 "$root/tools/ps5sh.py" "$host" ps 2>/dev/null | awk -v t="$title_id" '$8 == t {print $1}' || true)
|
||||
if [[ -z $pid ]]; then
|
||||
echo "==> [stop] $title_id is not running"
|
||||
exit 0
|
||||
fi
|
||||
echo "==> [stop] killing $title_id (pid $pid)"
|
||||
timeout 30 python3 "$root/tools/ps5sh.py" "$host" "kill $pid" >/dev/null 2>&1 || true
|
||||
sleep 3
|
||||
if timeout 30 python3 "$root/tools/ps5sh.py" "$host" ps 2>/dev/null | awk -v t="$title_id" '$8 == t' | grep -q .; then
|
||||
echo "==> [stop] still running" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "==> [stop] stopped"
|
||||
Reference in new issue
Block a user