mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 19:11:08 +00:00
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
349 lines
14 KiB
Diff
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,
|