Files
DroidDeck/tools/xbox/droiddeck-credential-fallback.patch
T
The412BannerandKeegantheVeganat0r 5ca1f610ab Xbox: fix sign-in on phones with a slow StrongBox Keystore (#1521)
On some phones (tester: Honor BKQ-N49, Android 17) Xbox sign-in stopped with
"the Xbox helper stopped with exit code 1" before Microsoft's page opened.
The backend asks the app to read and save credentials over a socket and waits
10 s for each reply. The app sealed credentials with a StrongBox-backed
Keystore key, and each StrongBox operation took 7-15 s on that phone, so the
backend gave up. The app still finished those saves afterwards, leaving a
record without a username that every later attempt had to read back slowly.

- Seal new store credentials with a TEE Keystore key (v2 envelopes); v1
  envelopes still open with the old key and move to v2 on their next write.
- A sign-in discards an Xbox record without a username (an unfinished
  sign-in) before starting the backend.
- The backend waits 30 s for the app's credential reply.
- The backend names the failing step instead of only credential-io (socket
  connect/exchange, session lock, missing socket name/port/key), and falls
  back to a keyed 127.0.0.1 port when the abstract socket is refused.
- The app's credential listener logs and retries accept errors, serves each
  connection on its own thread, and logs slow requests with per-stage times.

Refs #1337, #1300


Co-authored-by: KeegantheVeganat0r <58369898+KeegantheVeganat0r@users.noreply.github.com>
2026-10-25 01:39:09 +00:00

114 lines
5.2 KiB
Diff

diff --git a/crates/xodus/src/tokens/backend/keychain.rs b/crates/xodus/src/tokens/backend/keychain.rs
index 1903638..19480da 100644
--- a/crates/xodus/src/tokens/backend/keychain.rs
+++ b/crates/xodus/src/tokens/backend/keychain.rs
@@ -1,20 +1,46 @@
use crate::tokens::store::{TokenBackend, TokenStoreError};
use serde_json::{Value, json};
use std::io::{BufRead, Read, Write};
+use std::net::{Ipv4Addr, SocketAddrV4, TcpStream};
use std::os::linux::net::SocketAddrExt;
use std::os::unix::net::{SocketAddr, UnixStream};
+use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
pub struct KeychainBackend;
-fn request(value: Value) -> Result<Value, TokenStoreError> {
- let name = std::env::var("DROIDDECK_XBOX_CREDENTIAL_SOCKET")
- .map_err(|_| std::io::Error::other("Android credential host is unavailable"))?;
- let address = SocketAddr::from_abstract_name(name)?;
- let mut stream = UnixStream::connect_addr(&address)?;
- stream.set_read_timeout(Some(Duration::from_secs(10)))?;
- stream.set_write_timeout(Some(Duration::from_secs(10)))?;
- serde_json::to_writer(&mut stream, &value)?;
+// Some Android devices refuse the abstract socket from inside PRoot; after one failure the
+// keyed loopback port the app also listens on is used for the rest of the process.
+static LOOPBACK: AtomicBool = AtomicBool::new(false);
+static REPORTED: AtomicBool = AtomicBool::new(false);
+
+/// Letters and dashes only: the app passes `NEXUS_CREDENTIAL_ERROR_V1 [a-z-]{1,48}` lines to its log.
+fn reason(error: &std::io::Error) -> &'static str {
+ match error.raw_os_error() {
+ Some(111) => "refused",
+ Some(13) => "denied",
+ Some(1) => "not-permitted",
+ Some(2) => "missing",
+ Some(110) => "timed-out",
+ Some(38) => "unsupported",
+ Some(97) => "family-unsupported",
+ Some(22) => "invalid",
+ Some(104) => "reset",
+ Some(32) => "broken-pipe",
+ _ => match error.kind() {
+ std::io::ErrorKind::TimedOut | std::io::ErrorKind::WouldBlock => "timed-out",
+ std::io::ErrorKind::UnexpectedEof => "closed",
+ _ => "other",
+ },
+ }
+}
+
+fn exchange<S: Read + Write>(mut stream: S, prefix: Option<&str>, value: &Value) -> Result<Value, TokenStoreError> {
+ if let Some(key) = prefix {
+ stream.write_all(key.as_bytes())?;
+ stream.write_all(b"\n")?;
+ }
+ serde_json::to_writer(&mut stream, value)?;
stream.write_all(b"\n")?;
let mut bytes = Vec::new();
std::io::BufReader::new(stream).take(2_097_153).read_until(b'\n', &mut bytes)?;
@@ -22,10 +48,52 @@ fn request(value: Value) -> Result<Value, TokenStoreError> {
return Err(std::io::Error::other("Invalid Android credential response").into());
}
let response: Value = serde_json::from_slice(&bytes)?;
- if response["ok"] != true { return Err(std::io::Error::other("Android credential storage unavailable").into()); }
+ if response["ok"] != true {
+ eprintln!("NEXUS_CREDENTIAL_ERROR_V1 host-refused");
+ return Err(std::io::Error::other("Android credential storage unavailable").into());
+ }
Ok(response)
}
+fn loopback(value: &Value) -> Result<Value, TokenStoreError> {
+ let port: u16 = std::env::var("DROIDDECK_XBOX_CREDENTIAL_PORT").ok().and_then(|v| v.parse().ok())
+ .ok_or_else(|| std::io::Error::other("Android credential port is unavailable"))?;
+ let key = std::env::var("DROIDDECK_XBOX_CREDENTIAL_KEY")
+ .map_err(|_| std::io::Error::other("Android credential key is unavailable"))?;
+ let address = SocketAddrV4::new(Ipv4Addr::LOCALHOST, port).into();
+ let stream = TcpStream::connect_timeout(&address, Duration::from_secs(5)).inspect_err(|error| {
+ eprintln!("NEXUS_CREDENTIAL_ERROR_V1 loopback-{}", reason(error));
+ })?;
+ stream.set_read_timeout(Some(Duration::from_secs(10)))?;
+ stream.set_write_timeout(Some(Duration::from_secs(10)))?;
+ exchange(stream, Some(&key), value)
+}
+
+fn request(value: Value) -> Result<Value, TokenStoreError> {
+ if LOOPBACK.load(Ordering::Relaxed) { return loopback(&value); }
+ let name = std::env::var("DROIDDECK_XBOX_CREDENTIAL_SOCKET")
+ .map_err(|_| std::io::Error::other("Android credential host is unavailable"))?;
+ let address = SocketAddr::from_abstract_name(name)?;
+ match UnixStream::connect_addr(&address) {
+ Ok(stream) => {
+ stream.set_read_timeout(Some(Duration::from_secs(10)))?;
+ stream.set_write_timeout(Some(Duration::from_secs(10)))?;
+ exchange(stream, None, &value)
+ }
+ Err(error) => {
+ if !REPORTED.swap(true, Ordering::Relaxed) {
+ eprintln!("NEXUS_CREDENTIAL_ERROR_V1 socket-{}", reason(&error));
+ }
+ let result = loopback(&value);
+ if result.is_ok() {
+ LOOPBACK.store(true, Ordering::Relaxed);
+ eprintln!("NEXUS_CREDENTIAL_ERROR_V1 loopback-in-use");
+ }
+ result
+ }
+ }
+}
+
impl TokenBackend for KeychainBackend {
fn get(&self, key: &str) -> Result<Option<Vec<u8>>, TokenStoreError> {
let response = request(json!({"op":"get", "key":key}))?;