1
0
Fork 0
mirror of https://github.com/librespot-org/librespot.git synced 2025-10-05 10:49:40 +02:00

Solve hash cash challenges (experimental)

This commit is contained in:
Roderick van Domburg 2022-08-26 01:51:00 +02:00
parent 42a665fb0d
commit 7b19d4c1dd
No known key found for this signature in database
GPG key ID: 87F5FDE8A56219F4
6 changed files with 138 additions and 37 deletions

View file

@ -4,30 +4,37 @@ use std::{
time::{Duration, Instant},
};
use byteorder::{BigEndian, ByteOrder};
use bytes::Bytes;
use futures_util::future::IntoStream;
use http::header::HeaderValue;
use hyper::{
client::ResponseFuture,
header::{HeaderName, ACCEPT, AUTHORIZATION, CONTENT_ENCODING, CONTENT_TYPE, RANGE},
header::{HeaderName, ACCEPT, AUTHORIZATION, CONTENT_TYPE, RANGE},
Body, HeaderMap, Method, Request,
};
use protobuf::Message;
use protobuf::{Message, ProtobufEnum};
use rand::Rng;
use sha1::{Digest, Sha1};
use thiserror::Error;
use crate::{
apresolve::SocketAddress,
cdn_url::CdnUrl,
config::KEYMASTER_CLIENT_ID,
error::ErrorKind,
protocol::{
canvaz::EntityCanvazRequest,
clienttoken_http::{ClientTokenRequest, ClientTokenRequestType, ClientTokenResponse},
clienttoken_http::{
ChallengeAnswer, ChallengeType, ClientTokenRequest, ClientTokenRequestType,
ClientTokenResponse, ClientTokenResponseType,
},
connect::PutStateRequest,
extended_metadata::BatchedEntityRequest,
},
token::Token,
version, Error, FileId, SpotifyId,
version::spotify_version,
Error, FileId, SpotifyId,
};
component! {
@ -99,6 +106,42 @@ impl SpClient {
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> {
let client_token = self.lock(|inner| {
if let Some(token) = &inner.client_token {
@ -119,8 +162,8 @@ impl SpClient {
message.set_request_type(ClientTokenRequestType::REQUEST_CLIENT_DATA_REQUEST);
let client_data = message.mut_client_data();
client_data.set_client_id(self.session().client_id());
client_data.set_client_version(version::SEMVER.to_string());
client_data.set_client_id(KEYMASTER_CLIENT_ID.to_string());
client_data.set_client_version(spotify_version());
let connectivity_data = client_data.mut_connectivity_sdk_data();
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();
linux_data.set_system_name("Linux".to_string());
linux_data.set_system_release("5.4.0-56-generic".to_string());
linux_data
.set_system_version("#62-Ubuntu SMP Mon Nov 23 19:20:19 UTC 2020".to_string());
linux_data.set_system_release("5.15.0-46-generic".to_string());
linux_data.set_system_version(
"#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());
}
}
let body = message.write_to_bytes()?;
let mut response = self.client_token_request(&message).await?;
let request = Request::builder()
.method(&Method::POST)
.uri("https://clienttoken.spotify.com/v1/clienttoken")
.header(ACCEPT, HeaderValue::from_static("application/x-protobuf"))
.header(CONTENT_ENCODING, HeaderValue::from_static(""))
.body(Body::from(body))?;
let token_response = loop {
let message = ClientTokenResponse::parse_from_bytes(&response)?;
match ClientTokenResponseType::from_i32(message.response_type.value()) {
// depending on the platform, you're either given a token immediately
// or are presented a hash cash challenge to solve first
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 message = ClientTokenResponse::parse_from_bytes(&response)?;
let challenges = message.get_challenges().clone();
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 access_token = message.get_granted_token().get_token().to_owned();
let ctx = vec![];
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 {
access_token: access_token.clone(),
expires_in: Duration::from_secs(
message
.get_granted_token()
granted_token
.get_refresh_after_seconds()
.try_into()
.unwrap_or(7200),
),
token_type: "client-token".to_string(),
scopes: message
.get_granted_token()
scopes: granted_token
.get_domains()
.iter()
.map(|d| d.domain.clone())
@ -210,13 +305,12 @@ impl SpClient {
timestamp: Instant::now(),
};
trace!("Got client token: {:?}", 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(