mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Solve hash cash challenges (experimental)
This commit is contained in:
parent
42a665fb0d
commit
7b19d4c1dd
6 changed files with 138 additions and 37 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1404,6 +1404,7 @@ dependencies = [
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
"hex",
|
||||||
"hmac",
|
"hmac",
|
||||||
"http",
|
"http",
|
||||||
"httparse",
|
"httparse",
|
||||||
|
@ -3065,6 +3066,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
|
checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
|
"rand",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -22,6 +22,7 @@ dns-sd = { version = "0.1", optional = true }
|
||||||
form_urlencoded = "1.0"
|
form_urlencoded = "1.0"
|
||||||
futures-core = "0.3"
|
futures-core = "0.3"
|
||||||
futures-util = { version = "0.3", features = ["alloc", "bilock", "sink", "unstable"] }
|
futures-util = { version = "0.3", features = ["alloc", "bilock", "sink", "unstable"] }
|
||||||
|
hex = "0.4"
|
||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
httparse = "1.7"
|
httparse = "1.7"
|
||||||
http = "0.2"
|
http = "0.2"
|
||||||
|
@ -53,7 +54,7 @@ tokio-stream = "0.1"
|
||||||
tokio-tungstenite = { version = "*", default-features = false, features = ["rustls-tls-native-roots"] }
|
tokio-tungstenite = { version = "*", default-features = false, features = ["rustls-tls-native-roots"] }
|
||||||
tokio-util = { version = "0.7", features = ["codec"] }
|
tokio-util = { version = "0.7", features = ["codec"] }
|
||||||
url = "2"
|
url = "2"
|
||||||
uuid = { version = "1", default-features = false, features = ["v4"] }
|
uuid = { version = "1", default-features = false, features = ["fast-rng", "v4"] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::{fmt, path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
const KEYMASTER_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd";
|
pub(crate) const KEYMASTER_CLIENT_ID: &str = "65b708073fc0480ea92a077233ca87bd";
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct SessionConfig {
|
pub struct SessionConfig {
|
||||||
|
|
|
@ -15,7 +15,7 @@ use thiserror::Error;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
version::{FALLBACK_USER_AGENT, SPOTIFY_MOBILE_VERSION, SPOTIFY_VERSION, VERSION_STRING},
|
version::{spotify_version, FALLBACK_USER_AGENT, VERSION_STRING},
|
||||||
Error,
|
Error,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -82,11 +82,6 @@ pub struct HttpClient {
|
||||||
|
|
||||||
impl HttpClient {
|
impl HttpClient {
|
||||||
pub fn new(proxy_url: Option<&Url>) -> Self {
|
pub fn new(proxy_url: Option<&Url>) -> Self {
|
||||||
let spotify_version = match OS {
|
|
||||||
"android" | "ios" => SPOTIFY_MOBILE_VERSION.to_owned(),
|
|
||||||
_ => SPOTIFY_VERSION.to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let spotify_platform = match OS {
|
let spotify_platform = match OS {
|
||||||
"android" => "Android/31",
|
"android" => "Android/31",
|
||||||
"ios" => "iOS/15.2.1",
|
"ios" => "iOS/15.2.1",
|
||||||
|
@ -97,7 +92,9 @@ impl HttpClient {
|
||||||
|
|
||||||
let user_agent_str = &format!(
|
let user_agent_str = &format!(
|
||||||
"Spotify/{} {} ({})",
|
"Spotify/{} {} ({})",
|
||||||
spotify_version, spotify_platform, VERSION_STRING
|
spotify_version(),
|
||||||
|
spotify_platform,
|
||||||
|
VERSION_STRING
|
||||||
);
|
);
|
||||||
|
|
||||||
let user_agent = HeaderValue::from_str(user_agent_str).unwrap_or_else(|err| {
|
let user_agent = HeaderValue::from_str(user_agent_str).unwrap_or_else(|err| {
|
||||||
|
|
|
@ -4,30 +4,37 @@ use std::{
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use byteorder::{BigEndian, ByteOrder};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_util::future::IntoStream;
|
use futures_util::future::IntoStream;
|
||||||
use http::header::HeaderValue;
|
use http::header::HeaderValue;
|
||||||
use hyper::{
|
use hyper::{
|
||||||
client::ResponseFuture,
|
client::ResponseFuture,
|
||||||
header::{HeaderName, ACCEPT, AUTHORIZATION, CONTENT_ENCODING, CONTENT_TYPE, RANGE},
|
header::{HeaderName, ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE},
|
||||||
Body, HeaderMap, Method, Request,
|
Body, HeaderMap, Method, Request,
|
||||||
};
|
};
|
||||||
use protobuf::Message;
|
use protobuf::{Message, ProtobufEnum};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
|
use sha1::{Digest, Sha1};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
apresolve::SocketAddress,
|
apresolve::SocketAddress,
|
||||||
cdn_url::CdnUrl,
|
cdn_url::CdnUrl,
|
||||||
|
config::KEYMASTER_CLIENT_ID,
|
||||||
error::ErrorKind,
|
error::ErrorKind,
|
||||||
protocol::{
|
protocol::{
|
||||||
canvaz::EntityCanvazRequest,
|
canvaz::EntityCanvazRequest,
|
||||||
clienttoken_http::{ClientTokenRequest, ClientTokenRequestType, ClientTokenResponse},
|
clienttoken_http::{
|
||||||
|
ChallengeAnswer, ChallengeType, ClientTokenRequest, ClientTokenRequestType,
|
||||||
|
ClientTokenResponse, ClientTokenResponseType,
|
||||||
|
},
|
||||||
connect::PutStateRequest,
|
connect::PutStateRequest,
|
||||||
extended_metadata::BatchedEntityRequest,
|
extended_metadata::BatchedEntityRequest,
|
||||||
},
|
},
|
||||||
token::Token,
|
token::Token,
|
||||||
version, Error, FileId, SpotifyId,
|
version::spotify_version,
|
||||||
|
Error, FileId, SpotifyId,
|
||||||
};
|
};
|
||||||
|
|
||||||
component! {
|
component! {
|
||||||
|
@ -99,6 +106,42 @@ impl SpClient {
|
||||||
Ok(format!("https://{}:{}", ap.0, ap.1))
|
Ok(format!("https://{}:{}", ap.0, ap.1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn solve_hash_cash(ctx: &[u8], prefix: &[u8], length: i32, dst: &mut [u8]) {
|
||||||
|
let md = Sha1::digest(ctx);
|
||||||
|
|
||||||
|
let mut counter: i64 = 0;
|
||||||
|
let target: i64 = BigEndian::read_i64(&md[12..20]);
|
||||||
|
|
||||||
|
let suffix = loop {
|
||||||
|
let suffix = [(target + counter).to_be_bytes(), counter.to_be_bytes()].concat();
|
||||||
|
|
||||||
|
let mut hasher = Sha1::new();
|
||||||
|
hasher.update(prefix);
|
||||||
|
hasher.update(&suffix);
|
||||||
|
let md = hasher.finalize();
|
||||||
|
|
||||||
|
if BigEndian::read_i64(&md[12..20]).trailing_zeros() >= (length as u32) {
|
||||||
|
break suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
counter += 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
dst.copy_from_slice(&suffix);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn client_token_request(&self, message: &dyn Message) -> Result<Bytes, Error> {
|
||||||
|
let body = message.write_to_bytes()?;
|
||||||
|
|
||||||
|
let request = Request::builder()
|
||||||
|
.method(&Method::POST)
|
||||||
|
.uri("https://clienttoken.spotify.com/v1/clienttoken")
|
||||||
|
.header(ACCEPT, HeaderValue::from_static("application/x-protobuf"))
|
||||||
|
.body(Body::from(body))?;
|
||||||
|
|
||||||
|
self.session().http_client().request_body(request).await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn client_token(&self) -> Result<String, Error> {
|
pub async fn client_token(&self) -> Result<String, Error> {
|
||||||
let client_token = self.lock(|inner| {
|
let client_token = self.lock(|inner| {
|
||||||
if let Some(token) = &inner.client_token {
|
if let Some(token) = &inner.client_token {
|
||||||
|
@ -119,8 +162,8 @@ impl SpClient {
|
||||||
message.set_request_type(ClientTokenRequestType::REQUEST_CLIENT_DATA_REQUEST);
|
message.set_request_type(ClientTokenRequestType::REQUEST_CLIENT_DATA_REQUEST);
|
||||||
|
|
||||||
let client_data = message.mut_client_data();
|
let client_data = message.mut_client_data();
|
||||||
client_data.set_client_id(self.session().client_id());
|
client_data.set_client_id(KEYMASTER_CLIENT_ID.to_string());
|
||||||
client_data.set_client_version(version::SEMVER.to_string());
|
client_data.set_client_version(spotify_version());
|
||||||
|
|
||||||
let connectivity_data = client_data.mut_connectivity_sdk_data();
|
let connectivity_data = client_data.mut_connectivity_sdk_data();
|
||||||
connectivity_data.set_device_id(self.session().device_id().to_string());
|
connectivity_data.set_device_id(self.session().device_id().to_string());
|
||||||
|
@ -169,40 +212,92 @@ impl SpClient {
|
||||||
_ => {
|
_ => {
|
||||||
let linux_data = platform_data.mut_desktop_linux();
|
let linux_data = platform_data.mut_desktop_linux();
|
||||||
linux_data.set_system_name("Linux".to_string());
|
linux_data.set_system_name("Linux".to_string());
|
||||||
linux_data.set_system_release("5.4.0-56-generic".to_string());
|
linux_data.set_system_release("5.15.0-46-generic".to_string());
|
||||||
linux_data
|
linux_data.set_system_version(
|
||||||
.set_system_version("#62-Ubuntu SMP Mon Nov 23 19:20:19 UTC 2020".to_string());
|
"#49~20.04.1-Ubuntu SMP Thu Aug 4 19:15:44 UTC 2022".to_string(),
|
||||||
|
);
|
||||||
linux_data.set_hardware(std::env::consts::ARCH.to_string());
|
linux_data.set_hardware(std::env::consts::ARCH.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let body = message.write_to_bytes()?;
|
let mut response = self.client_token_request(&message).await?;
|
||||||
|
|
||||||
let request = Request::builder()
|
let token_response = loop {
|
||||||
.method(&Method::POST)
|
let message = ClientTokenResponse::parse_from_bytes(&response)?;
|
||||||
.uri("https://clienttoken.spotify.com/v1/clienttoken")
|
match ClientTokenResponseType::from_i32(message.response_type.value()) {
|
||||||
.header(ACCEPT, HeaderValue::from_static("application/x-protobuf"))
|
// depending on the platform, you're either given a token immediately
|
||||||
.header(CONTENT_ENCODING, HeaderValue::from_static(""))
|
// or are presented a hash cash challenge to solve first
|
||||||
.body(Body::from(body))?;
|
Some(ClientTokenResponseType::RESPONSE_GRANTED_TOKEN_RESPONSE) => break message,
|
||||||
|
Some(ClientTokenResponseType::RESPONSE_CHALLENGES_RESPONSE) => {
|
||||||
|
trace!("received a hash cash challenge");
|
||||||
|
|
||||||
let response = self.session().http_client().request_body(request).await?;
|
let challenges = message.get_challenges().clone();
|
||||||
let message = ClientTokenResponse::parse_from_bytes(&response)?;
|
let state = challenges.get_state();
|
||||||
|
if let Some(challenge) = challenges.challenges.first() {
|
||||||
|
let hash_cash_challenge = challenge.get_evaluate_hashcash_parameters();
|
||||||
|
|
||||||
let client_token = self.lock(|inner| {
|
let ctx = vec![];
|
||||||
let access_token = message.get_granted_token().get_token().to_owned();
|
let prefix = hex::decode(&hash_cash_challenge.prefix).map_err(|e| {
|
||||||
|
Error::failed_precondition(format!(
|
||||||
|
"unable to decode Hashcash challenge: {}",
|
||||||
|
e
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let length = hash_cash_challenge.length;
|
||||||
|
|
||||||
|
let mut suffix = vec![0; 0x10];
|
||||||
|
Self::solve_hash_cash(&ctx, &prefix, length, &mut suffix);
|
||||||
|
|
||||||
|
// the suffix must be in uppercase
|
||||||
|
let suffix = hex::encode(suffix).to_uppercase();
|
||||||
|
|
||||||
|
let mut answer_message = ClientTokenRequest::new();
|
||||||
|
answer_message.set_request_type(
|
||||||
|
ClientTokenRequestType::REQUEST_CHALLENGE_ANSWERS_REQUEST,
|
||||||
|
);
|
||||||
|
|
||||||
|
let challenge_answers = answer_message.mut_challenge_answers();
|
||||||
|
|
||||||
|
let mut challenge_answer = ChallengeAnswer::new();
|
||||||
|
challenge_answer.mut_hash_cash().suffix = suffix.to_string();
|
||||||
|
challenge_answer.ChallengeType = ChallengeType::CHALLENGE_HASH_CASH;
|
||||||
|
|
||||||
|
challenge_answers.state = state.to_string();
|
||||||
|
challenge_answers.answers.push(challenge_answer);
|
||||||
|
|
||||||
|
response = self.client_token_request(&answer_message).await?;
|
||||||
|
|
||||||
|
// we should have been granted a token now
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
return Err(Error::failed_precondition("no challenges found"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(unknown) => {
|
||||||
|
return Err(Error::unimplemented(format!(
|
||||||
|
"unknown client token response type: {:?}",
|
||||||
|
unknown
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
None => return Err(Error::failed_precondition("no client token response type")),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let granted_token = token_response.get_granted_token();
|
||||||
|
let access_token = granted_token.get_token().to_owned();
|
||||||
|
|
||||||
|
self.lock(|inner| {
|
||||||
let client_token = Token {
|
let client_token = Token {
|
||||||
access_token: access_token.clone(),
|
access_token: access_token.clone(),
|
||||||
expires_in: Duration::from_secs(
|
expires_in: Duration::from_secs(
|
||||||
message
|
granted_token
|
||||||
.get_granted_token()
|
|
||||||
.get_refresh_after_seconds()
|
.get_refresh_after_seconds()
|
||||||
.try_into()
|
.try_into()
|
||||||
.unwrap_or(7200),
|
.unwrap_or(7200),
|
||||||
),
|
),
|
||||||
token_type: "client-token".to_string(),
|
token_type: "client-token".to_string(),
|
||||||
scopes: message
|
scopes: granted_token
|
||||||
.get_granted_token()
|
|
||||||
.get_domains()
|
.get_domains()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|d| d.domain.clone())
|
.map(|d| d.domain.clone())
|
||||||
|
@ -210,13 +305,12 @@ impl SpClient {
|
||||||
timestamp: Instant::now(),
|
timestamp: Instant::now(),
|
||||||
};
|
};
|
||||||
|
|
||||||
trace!("Got client token: {:?}", client_token);
|
|
||||||
|
|
||||||
inner.client_token = Some(client_token);
|
inner.client_token = Some(client_token);
|
||||||
access_token
|
|
||||||
});
|
});
|
||||||
|
|
||||||
Ok(client_token)
|
trace!("Got client token: {:?}", client_token);
|
||||||
|
|
||||||
|
Ok(access_token)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn request_with_protobuf(
|
pub async fn request_with_protobuf(
|
||||||
|
|
|
@ -24,3 +24,10 @@ pub const SPOTIFY_MOBILE_VERSION: &str = "8.6.84";
|
||||||
|
|
||||||
/// The user agent to fall back to, if one could not be determined dynamically.
|
/// The user agent to fall back to, if one could not be determined dynamically.
|
||||||
pub const FALLBACK_USER_AGENT: &str = "Spotify/117300517 Linux/0 (librespot)";
|
pub const FALLBACK_USER_AGENT: &str = "Spotify/117300517 Linux/0 (librespot)";
|
||||||
|
|
||||||
|
pub fn spotify_version() -> String {
|
||||||
|
match std::env::consts::OS {
|
||||||
|
"android" | "ios" => SPOTIFY_MOBILE_VERSION.to_owned(),
|
||||||
|
_ => SPOTIFY_VERSION.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue