Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 0 additions & 18 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["psst-protocol", "psst-core", "psst-cli", "psst-gui"]
members = ["psst-core", "psst-cli", "psst-gui"]

[profile.dev]
opt-level = 1
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,6 @@ Here's the basic project structure:
- `/psst-core` - Core library, takes care of Spotify TCP session, audio file retrieval, decoding, audio output, playback queue, etc.
- `/psst-gui` - GUI application built with [Druid](https://github.com/linebender/druid)
- `/psst-cli` - Example CLI that plays a track. Credentials must be configured in the code.
- `/psst-protocol` - Internal Protobuf definitions used for Spotify communication.

## Privacy Policy

Expand Down
14 changes: 5 additions & 9 deletions psst-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,6 @@ gix-config = "0.45.1"
time = { version = "0.3.36", features = ["local-offset"] }

[dependencies]
psst-protocol = { path = "../psst-protocol" }
rustfm-scrobble = "1.1.1"

# Login5 dependencies
librespot-protocol = "0.7.1"
protobuf = "3"
sysinfo = "0.35.0"
data-encoding = "2.9"

# Common
byteorder = { version = "1.5.0" }
Expand All @@ -28,13 +20,17 @@ num-bigint = { version = "0.4.6", features = ["rand"] }
num-traits = { version = "0.2.19" }
oauth2 = { version = "4.4.2" }
parking_lot = { version = "0.12.3" }
quick-protobuf = { version = "0.8.1" }
librespot-protocol = "0.7.1"
protobuf = "3"
sysinfo = "0.35.0"
data-encoding = "2.9"
rand = { version = "0.9.1" }
rangemap = { version = "1.5.1" }
serde = { version = "1.0.210", features = ["derive"] }
serde_json = { version = "1.0.132" }
socks = { version = "0.3.4" }
tempfile = { version = "3.13.0" }
rustfm-scrobble = "1.1.1"
ureq = { version = "3.0.11", features = ["json"] }
url = { version = "2.5.2" }

Expand Down
13 changes: 7 additions & 6 deletions psst-core/src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ use crate::{
audio::decrypt::AudioKey,
error::Error,
item_id::{FileId, ItemId},
protocol::metadata::{Episode, Track},
util::{deserialize_protobuf, serialize_protobuf},
};

use librespot_protocol::metadata::{Episode, Track};
use protobuf::Message;

pub type CacheHandle = Arc<Cache>;

#[derive(Debug)]
Expand Down Expand Up @@ -61,12 +62,12 @@ impl Cache {
impl Cache {
pub fn get_track(&self, item_id: ItemId) -> Option<Track> {
let buf = fs::read(self.track_path(item_id)).ok()?;
deserialize_protobuf(&buf).ok()
Track::parse_from_bytes(&buf).ok()
}

pub fn save_track(&self, item_id: ItemId, track: &Track) -> Result<(), Error> {
log::debug!("saving track to cache: {item_id:?}");
fs::write(self.track_path(item_id), serialize_protobuf(track)?)?;
fs::write(self.track_path(item_id), track.write_to_bytes()?)?;
Ok(())
}

Expand All @@ -79,12 +80,12 @@ impl Cache {
impl Cache {
pub fn get_episode(&self, item_id: ItemId) -> Option<Episode> {
let buf = fs::read(self.episode_path(item_id)).ok()?;
deserialize_protobuf(&buf).ok()
Episode::parse_from_bytes(&buf).ok()
}

pub fn save_episode(&self, item_id: ItemId, episode: &Episode) -> Result<(), Error> {
log::debug!("saving episode to cache: {item_id:?}");
fs::write(self.episode_path(item_id), serialize_protobuf(episode)?)?;
fs::write(self.episode_path(item_id), episode.write_to_bytes()?)?;
Ok(())
}

Expand Down
119 changes: 69 additions & 50 deletions psst-core/src/connection/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,12 @@ use crate::{
shannon_codec::{ShannonDecoder, ShannonEncoder, ShannonMsg},
},
error::Error,
protocol::authentication::AuthenticationType,
util::{
default_ureq_agent_builder, deserialize_protobuf, serialize_protobuf, NET_CONNECT_TIMEOUT,
NET_IO_TIMEOUT,
},
util::{default_ureq_agent_builder, NET_CONNECT_TIMEOUT, NET_IO_TIMEOUT},
};

use librespot_protocol::authentication::AuthenticationType;
use protobuf::{Enum, Message, MessageField, SpecialFields};

// Device ID used for authentication message.
const DEVICE_ID: &str = "Psst";

Expand Down Expand Up @@ -76,7 +75,7 @@ impl From<SerializedCredentials> for Credentials {
Self {
username: Some(value.username),
auth_data: value.auth_data.into_bytes(),
auth_type: value.auth_type.into(),
auth_type: AuthenticationType::from_i32(value.auth_type).unwrap_or_default(),
}
}
}
Expand Down Expand Up @@ -215,7 +214,7 @@ impl Transport {
}

pub fn exchange_keys(mut stream: TcpStream) -> Result<Self, Error> {
use crate::protocol::keyexchange::APResponseMessage;
use librespot_protocol::keyexchange::APResponseMessage;

let local_keys = DHLocalKeys::random();

Expand All @@ -232,17 +231,18 @@ impl Transport {
// hashed together with the shared secret to make a key pair).
log::trace!("waiting for AP response");
let apresp_packet = read_packet(&mut stream)?;
let apresp: APResponseMessage = deserialize_protobuf(&apresp_packet[4..])?;
let apresp = APResponseMessage::parse_from_bytes(&apresp_packet[4..])?;
log::trace!("received AP response");

// Compute the challenge response and the sending/receiving keys.
let remote_key = &apresp
let remote_key = apresp
.challenge
.expect("Missing data")
.login_crypto_challenge
.diffie_hellman
.expect("Missing data")
.gs;
.gs
.as_ref()
.expect("Missing data");

let (challenge, send_key, recv_key) = compute_keys(
&local_keys.shared_secret(remote_key),
&hello_packet,
Expand All @@ -268,7 +268,7 @@ impl Transport {
}

pub fn authenticate(&mut self, credentials: Credentials) -> Result<Credentials, Error> {
use crate::protocol::{authentication::APWelcome, keyexchange::APLoginFailed};
use librespot_protocol::{authentication::APWelcome, keyexchange::APLoginFailed};

// Send a login request with the client credentials.
let request = client_response_encrypted(credentials);
Expand All @@ -279,19 +279,20 @@ impl Transport {

match response.cmd {
ShannonMsg::AP_WELCOME => {
let welcome_data: APWelcome =
deserialize_protobuf(&response.payload).expect("Missing data");
let welcome_data =
APWelcome::parse_from_bytes(&response.payload).expect("Missing data");

Ok(Credentials {
username: Some(welcome_data.canonical_username),
auth_data: welcome_data.reusable_auth_credentials,
auth_type: welcome_data.reusable_auth_credentials_type,
username: Some(welcome_data.canonical_username().to_string()),
auth_data: welcome_data.reusable_auth_credentials().to_vec(),
auth_type: welcome_data.reusable_auth_credentials_type(),
})
}
ShannonMsg::AUTH_FAILURE => {
let error_data: APLoginFailed =
deserialize_protobuf(&response.payload).expect("Missing data");
let error_data =
APLoginFailed::parse_from_bytes(&response.payload).expect("Missing data");
Err(Error::AuthFailed {
code: error_data.error_code as _,
code: error_data.error_code() as _,
})
}
_ => {
Expand Down Expand Up @@ -321,44 +322,57 @@ fn make_packet(prefix: &[u8], data: &[u8]) -> Vec<u8> {
}

fn client_hello(public_key: Vec<u8>, nonce: Vec<u8>) -> Vec<u8> {
use crate::protocol::keyexchange::*;
use librespot_protocol::keyexchange::*;

let hello = ClientHello {
build_info: BuildInfo {
platform: Platform::PLATFORM_LINUX_X86,
product: Product::PRODUCT_PARTNER,
build_info: MessageField::some(BuildInfo {
platform: Some(Platform::PLATFORM_LINUX_X86.into()),
product: Some(Product::PRODUCT_PARTNER.into()),
product_flags: vec![],
version: 109_800_078,
},
cryptosuites_supported: vec![Cryptosuite::CRYPTO_SUITE_SHANNON],
version: Some(109_800_078),
special_fields: SpecialFields::new(),
}),
cryptosuites_supported: vec![Cryptosuite::CRYPTO_SUITE_SHANNON.into()],
fingerprints_supported: vec![],
powschemes_supported: vec![],
login_crypto_hello: LoginCryptoHelloUnion {
diffie_hellman: Some(LoginCryptoDiffieHellmanHello {
gc: public_key,
server_keys_known: 1,
login_crypto_hello: MessageField::some(LoginCryptoHelloUnion {
diffie_hellman: MessageField::some(LoginCryptoDiffieHellmanHello {
gc: Some(public_key),
server_keys_known: Some(1),
special_fields: SpecialFields::new(),
}),
},
client_nonce: nonce,
special_fields: SpecialFields::new(),
}),
client_nonce: Some(nonce),
padding: Some(vec![0x1e]),
feature_set: None,
feature_set: None.into(),
special_fields: SpecialFields::new(),
};

serialize_protobuf(&hello).expect("Failed to serialize")
hello
.write_to_bytes()
.expect("Failed to serialize client hello")
}

fn client_response_plaintext(challenge: Vec<u8>) -> Vec<u8> {
use crate::protocol::keyexchange::*;
use librespot_protocol::keyexchange::*;

let response = ClientResponsePlaintext {
login_crypto_response: LoginCryptoResponseUnion {
diffie_hellman: Some(LoginCryptoDiffieHellmanResponse { hmac: challenge }),
},
pow_response: PoWResponseUnion::default(),
crypto_response: CryptoResponseUnion::default(),
login_crypto_response: MessageField::some(LoginCryptoResponseUnion {
diffie_hellman: MessageField::some(LoginCryptoDiffieHellmanResponse {
hmac: Some(challenge),
special_fields: SpecialFields::new(),
}),
special_fields: SpecialFields::new(),
}),
pow_response: MessageField::some(PoWResponseUnion::default()),
crypto_response: MessageField::some(CryptoResponseUnion::default()),
special_fields: SpecialFields::new(),
};

serialize_protobuf(&response).expect("Failed to serialize")
response
.write_to_bytes()
.expect("Failed to serialize client response")
}

fn compute_keys(
Expand Down Expand Up @@ -389,22 +403,27 @@ fn compute_keys(
}

fn client_response_encrypted(credentials: Credentials) -> ShannonMsg {
use crate::protocol::authentication::{ClientResponseEncrypted, LoginCredentials, SystemInfo};
use librespot_protocol::authentication::{
ClientResponseEncrypted, LoginCredentials, SystemInfo, Os, CpuFamily
};

let response = ClientResponseEncrypted {
login_credentials: LoginCredentials {
login_credentials: MessageField::some(LoginCredentials {
username: credentials.username,
auth_data: Some(credentials.auth_data),
typ: credentials.auth_type,
},
system_info: SystemInfo {
typ: Some(credentials.auth_type.into()),
special_fields: SpecialFields::new(),
}),
system_info: MessageField::some(SystemInfo {
device_id: Some(DEVICE_ID.to_string()),
system_information_string: Some("librespot_but_actually_psst".to_string()),
os: Some(Os::default().into()),
cpu_family: Some(CpuFamily::default().into()),
..SystemInfo::default()
},
}),
..ClientResponseEncrypted::default()
};

let buf = serialize_protobuf(&response).expect("Failed to serialize");
let buf = response.write_to_bytes().expect("Failed to serialize");
ShannonMsg::new(ShannonMsg::LOGIN, buf)
}
2 changes: 0 additions & 2 deletions psst-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,3 @@ pub mod player;
pub mod session;
pub mod system_info;
pub mod util;

pub use psst_protocol as protocol;
Loading