Files
DroidDeck/tools/xbox/droiddeck-credentials.patch
devSharaku e871557bf2 Update Xbox backend to 0.2.12 and accept compact JWE tickets (#1729)
Some Microsoft accounts get a delegation-compact (d=) ticket, which the
pinned Xbox backend (Nexus 0.2.8) rejected: the library stayed empty
(store-ticket-format) and downloads failed. This moves the backend to
Nexus 0.2.12, which has the upstream store-ticket fix, rebases the
DroidDeck credentials, launch-view and cloud-token patches, and strips
the d= prefix from compact JWE tickets so downloads work again.

Tested by seven users in the Discord Xbox Store Issues thread on the
pr-1729 build at d55e05a0.

* Update Xbox backend to 0.2.12
* Fix cloud offering fallback and add regression tests
* Fix Xbox authentication with compact JWE tickets
2026-10-29 00:07:53 +00:00

349 lines
14 KiB
Diff

--- a/crates/xodus/src/secrets.rs
+++ b/crates/xodus/src/secrets.rs
@@ -1,73 +1,14 @@
-pub static SERVICE_NAME: &str = "Xodus Service";
+use crate::tokens::{backend::KeychainBackend, store::{TokenBackend, TokenStoreError}};
pub fn init_secrets() -> Result<(), keyring_core::Error> {
- #[cfg(feature = "key-chain-file")]
- {
- let store = keyring_core::sample::Store::new_with_backing(
- secrets_backing_file()
- .to_str()
- .expect("Invalid secrets backing path"),
- )?;
- keyring_core::set_default_store(store);
- }
-
- #[cfg(not(feature = "key-chain-file"))]
- {
- #[cfg(target_os = "linux")]
- {
- activate_desktop_keyring();
- keyring_core::set_default_store(dbus_secret_service_keyring_store::Store::new()?);
- }
-
- #[cfg(target_os = "macos")]
- {
- keyring_core::set_default_store(apple_native_keyring_store::keychain::Store::new()?);
- }
-
- #[cfg(not(any(target_os = "linux", target_os = "macos")))]
- {
- let store = keyring_core::sample::Store::new_with_configuration(
- &std::collections::HashMap::from([("persist", "true")]),
- )?;
- keyring_core::set_default_store(store);
- }
- }
-
+ if std::env::var_os("DROIDDECK_XBOX_CREDENTIAL_SOCKET").is_none() { return Err(keyring_core::Error::NoEntry); }
Ok(())
}
-
-// Some KDE installations advertise ksecretd only under its compatibility
-// activation name. Respect an existing/default Secret Service provider first.
-// This starts a packaged user service, never installs or configures a keyring.
-#[cfg(all(target_os = "linux", not(feature = "key-chain-file")))]
-fn activate_desktop_keyring() {
- use dbus::blocking::Connection;
- use std::time::Duration;
- let Ok(connection) = Connection::new_session() else { return; };
- let bus = connection.with_proxy("org.freedesktop.DBus", "/org/freedesktop/DBus", Duration::from_secs(3));
- let Ok((owned,)): Result<(bool,), _> = bus.method_call("org.freedesktop.DBus", "NameHasOwner", ("org.freedesktop.secrets",)) else { return; };
- if owned { return; }
- let Ok((names,)): Result<(Vec<String>,), _> = bus.method_call("org.freedesktop.DBus", "ListActivatableNames", ()) else { return; };
- if names.iter().any(|name| name == "org.freedesktop.secrets") { return; }
- if names.iter().any(|name| name == "org.kde.secretservicecompat") {
- // On failure, Store::new below still returns the genuine keyring error.
- let _: Result<(u32,), _> = bus.method_call("org.freedesktop.DBus", "StartServiceByName", ("org.kde.secretservicecompat", 0u32));
- }
+pub struct Entry(String);
+pub fn get_entry(user: &str) -> Result<Entry, keyring_core::Error> { Ok(Entry(user.into())) }
+impl Entry {
+ pub fn get_secret(&self) -> Result<Vec<u8>, TokenStoreError> { KeychainBackend.get(&self.0)?.ok_or(TokenStoreError::NotFound) }
+ pub fn set_secret(&self, value: &[u8]) -> Result<(), TokenStoreError> { KeychainBackend.set(&self.0, value) }
+ pub fn delete_credential(&self) -> Result<(), TokenStoreError> { KeychainBackend.remove(&self.0) }
}
-
-pub fn get_entry(user: &str) -> Result<keyring_core::Entry, keyring_core::Error> {
- keyring_core::Entry::new(SERVICE_NAME, user)
-}
-
-pub fn destroy_secrets() {
- keyring_core::unset_default_store();
-}
-
-#[cfg(feature = "key-chain-file")]
-fn secrets_backing_file() -> std::path::PathBuf {
- std::env::var_os("HOME")
- .or_else(|| std::env::var_os("USERPROFILE"))
- .map(std::path::PathBuf::from)
- .unwrap_or_else(std::env::temp_dir)
- .join(".xodus-keyring.ron")
-}
+pub fn destroy_secrets() {}
--- a/crates/xodus/src/tokens/backend/keychain.rs
+++ b/crates/xodus/src/tokens/backend/keychain.rs
@@ -1,131 +1,43 @@
-use base64::{Engine as _, engine::general_purpose::STANDARD};
-
use crate::tokens::store::{TokenBackend, TokenStoreError};
-
-// GNOME Keyring's passwordless textual format writes secrets with
-// g_key_file_set_value but reads them with g_key_file_get_string. Backslash
-// escapes in JSON (notably XML token attributes) can therefore be rejected or
-// changed on reload. ASCII base64 avoids those escapes without changing the
-// selected keyring or its security policy. This is encoding, not encryption.
-const ENVELOPE: &[u8] = b"XODUS_KEYRING_BASE64_V1:";
-
-fn encode_secret(value: &[u8]) -> Vec<u8> {
- let mut encoded = ENVELOPE.to_vec();
- encoded.extend_from_slice(STANDARD.encode(value).as_bytes());
- encoded
-}
-
-fn decode_secret(bytes: Vec<u8>) -> Result<Vec<u8>, TokenStoreError> {
- match bytes.strip_prefix(ENVELOPE) {
- Some(encoded) => STANDARD.decode(encoded).map_err(|_| {
- std::io::Error::new(
- std::io::ErrorKind::InvalidData,
- "Invalid encoded Xodus credential",
- )
- .into()
- }),
- // Existing credentials are raw JSON. Do not rewrite or delete on reads.
- None => Ok(bytes),
- }
-}
-
-fn deletion_result(result: Result<(), keyring_core::Error>) -> Result<(), TokenStoreError> {
- match result {
- Ok(()) | Err(keyring_core::Error::NoEntry) => Ok(()),
- Err(error) => Err(error.into()),
- }
-}
+use serde_json::{Value, json};
+use std::io::{BufRead, Read, Write};
+use std::os::linux::net::SocketAddrExt;
+use std::os::unix::net::{SocketAddr, UnixStream};
+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)?;
+ 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)?;
+ if bytes.len() > 2_097_152 || bytes.last() != Some(&b'\n') {
+ 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()); }
+ Ok(response)
+}
+
impl TokenBackend for KeychainBackend {
fn get(&self, key: &str) -> Result<Option<Vec<u8>>, TokenStoreError> {
- let entry = crate::secrets::get_entry(key)?;
- match entry.get_secret() {
- Ok(bytes) => Ok(Some(decode_secret(bytes)?)),
- Err(keyring_core::Error::NoEntry) => Ok(None),
- Err(e) => Err(e.into()),
- }
+ let response = request(json!({"op":"get", "key":key}))?;
+ if response["value"].is_null() { return Ok(None); }
+ Ok(Some(serde_json::from_value(response["value"].clone())?))
}
-
fn set(&self, key: &str, value: &[u8]) -> Result<(), TokenStoreError> {
- crate::secrets::get_entry(key)?.set_secret(&encode_secret(value))?;
- #[cfg(all(target_os = "linux", not(feature = "key-chain-file")))]
- crate::keyring_persistence::sync_after_write();
+ request(json!({"op":"set", "key":key, "value":value}))?;
Ok(())
}
-
fn remove(&self, key: &str) -> Result<(), TokenStoreError> {
- deletion_result(crate::secrets::get_entry(key)?.delete_credential())?;
- #[cfg(all(target_os = "linux", not(feature = "key-chain-file")))]
- crate::keyring_persistence::sync_after_write();
+ request(json!({"op":"remove", "key":key}))?;
Ok(())
}
}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn credential_encoding_preserves_xml_json_and_binary() {
- let xml_json = br#"{"token":"<EncryptedData Id=\"synthetic\">dummy</EncryptedData>","escapes":"a\\b\n\t\r"}"#;
- let binary: Vec<u8> = (0..=255).collect();
- for value in [
- xml_json.as_slice(),
- b"".as_slice(),
- binary.as_slice(),
- "synthetic-\u{00e9}".as_bytes(),
- ] {
- let encoded = encode_secret(value);
- assert!(encoded.starts_with(ENVELOPE));
- // No GKeyFile escapes or physical line breaks can be introduced.
- assert!(encoded.is_ascii());
- assert!(
- !encoded
- .iter()
- .any(|byte| matches!(byte, b'\\' | b'\n' | b'\r' | 0))
- );
- assert_eq!(decode_secret(encoded).unwrap(), value);
- }
- }
-
- #[test]
- fn credential_encoding_accepts_legacy_values_without_rewriting() {
- for value in [
- br#"{"username":"synthetic"}"#.as_slice(),
- b"".as_slice(),
- b"invalid legacy JSON".as_slice(),
- ] {
- // Validation of legacy JSON remains the TokenManager's job.
- assert_eq!(decode_secret(value.to_vec()).unwrap(), value);
- }
- }
-
- #[test]
- fn credential_encoding_rejects_malformed_envelopes_without_leaking_values() {
- for bad in [
- b"%%%synthetic-secret-never-print".as_slice(),
- b"YQ".as_slice(),
- ] {
- let mut encoded = ENVELOPE.to_vec();
- encoded.extend_from_slice(bad);
- let error = decode_secret(encoded).unwrap_err();
- assert!(matches!(error, TokenStoreError::Io(_)));
- assert!(!error.to_string().contains("synthetic-secret-never-print"));
- }
- }
-
- #[test]
- fn credential_deletion_is_idempotent_but_keeps_other_errors() {
- assert!(deletion_result(Ok(())).is_ok());
- assert!(deletion_result(Err(keyring_core::Error::NoEntry)).is_ok());
- assert!(matches!(
- deletion_result(Err(keyring_core::Error::Invalid(
- "synthetic".into(),
- "failure".into()
- ))),
- Err(TokenStoreError::Keychain(_))
- ));
- }
-}
--- a/crates/xodus/Cargo.toml
+++ b/crates/xodus/Cargo.toml
@@ -43,9 +43,9 @@
smbios-lib = "0.9.4"
[target.'cfg(target_os = "linux")'.dependencies]
-dbus = "=0.9.12"
-x11-dl = "=2.21.0"
-dbus-secret-service-keyring-store = { version = "1.0.1", features = ["crypto-openssl"]}
+dbus = { version = "=0.9.12", optional = true }
+x11-dl = { version = "=2.21.0", optional = true }
+dbus-secret-service-keyring-store = { version = "1.0.1", features = ["crypto-openssl"], optional = true}
[target.'cfg(target_os = "macos")'.dependencies]
apple-native-keyring-store = { version = "1.0.2", features = ["keychain"]}
--- a/crates/xodus/src/lib.rs
+++ b/crates/xodus/src/lib.rs
@@ -2,12 +2,6 @@
pub mod auth;
pub mod clep;
pub mod hardware;
-#[cfg(target_os = "linux")]
-mod keyring_persistence;
-#[cfg(target_os = "linux")]
-pub mod keyring_session;
-#[cfg(target_os = "linux")]
-pub mod keyring_unlock;
pub mod licensing;
pub mod models;
pub mod secrets;
--- a/crates/xodus-cli/src/main.rs
+++ b/crates/xodus-cli/src/main.rs
@@ -18,24 +18,6 @@
#[derive(Subcommand)]
enum SubCommand {
- #[cfg(target_os = "linux")]
- #[command(about = "Gracefully stop only a Nexus-owned session keyring without accessing credentials")]
- KeyringSessionStop {
- #[arg(long)]
- provider_pid: u32,
- },
- #[cfg(target_os = "linux")]
- #[command(about = "Create the first default wallet using the provider's password dialog")]
- KeyringCreate {
- #[arg(long, default_value = "")]
- window_id: String,
- },
- #[cfg(target_os = "linux")]
- #[command(about = "Unlock the default system wallet without reading credentials or signing in")]
- KeyringUnlock {
- #[arg(long, default_value = "")]
- window_id: String,
- },
#[command(about = "Resolve public Store packages without sign-in or downloading")]
ResolvePackage {
product: String,
@@ -238,29 +220,6 @@
.build()
.unwrap();
let args = CliArgs::parse();
- #[cfg(target_os = "linux")]
- if let SubCommand::KeyringSessionStop { provider_pid } = &args.command {
- let state = xodus::keyring_session::stop_session_provider(*provider_pid);
- println!("{}", serde_json::json!({"schema": 1, "state": state.as_str()}));
- return if state == xodus::keyring_session::StopState::Unavailable { ExitCode::FAILURE } else { ExitCode::SUCCESS };
- }
- #[cfg(target_os = "linux")]
- if let SubCommand::KeyringUnlock { window_id } | SubCommand::KeyringCreate { window_id } = &args.command {
- use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
- let cancelled = Arc::new(AtomicBool::new(false));
- let signal = cancelled.clone();
- let state = if ctrlc::set_handler(move || { signal.store(true, Ordering::SeqCst); }).is_err() {
- xodus::keyring_unlock::UnlockState::Unavailable
- } else {
- if matches!(&args.command, SubCommand::KeyringCreate { .. }) {
- xodus::keyring_unlock::create_default(window_id, &cancelled)
- } else {
- xodus::keyring_unlock::unlock_default(window_id, &cancelled)
- }
- };
- println!("{}", serde_json::json!({"schema": 1, "state": state.as_str()}));
- return ExitCode::SUCCESS;
- }
#[cfg(not(feature = "native-webview"))]
if args.external_webview_stdio {
webview::enable_stdio();
@@ -308,12 +267,6 @@
}
let code = match args.command {
- #[cfg(target_os = "linux")]
- SubCommand::KeyringSessionStop { .. } => unreachable!(),
- #[cfg(target_os = "linux")]
- SubCommand::KeyringUnlock { .. } => unreachable!(),
- #[cfg(target_os = "linux")]
- SubCommand::KeyringCreate { .. } => unreachable!(),
SubCommand::ResolvePackage { .. } => unreachable!(),
SubCommand::Download {
product,