mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 11:01:07 +00:00
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>
114 lines
5.2 KiB
Diff
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}))?;
|