Make hash cash challenges a bit more robust

This commit is contained in:
Roderick van Domburg 2022-08-28 23:52:22 +02:00
parent 10650712a7
commit 49e885d158
No known key found for this signature in database
GPG key ID: 87F5FDE8A56219F4

View file

@ -108,13 +108,29 @@ 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]) { fn solve_hash_cash(
ctx: &[u8],
prefix: &[u8],
length: i32,
dst: &mut [u8],
) -> Result<(), Error> {
// after a certain number of seconds, the challenge expires
const TIMEOUT: u64 = 5; // seconds
let now = Instant::now();
let md = Sha1::digest(ctx); let md = Sha1::digest(ctx);
let mut counter: i64 = 0; let mut counter: i64 = 0;
let target: i64 = BigEndian::read_i64(&md[12..20]); let target: i64 = BigEndian::read_i64(&md[12..20]);
let suffix = loop { let suffix = loop {
if now.elapsed().as_secs() >= TIMEOUT {
return Err(Error::deadline_exceeded(format!(
"{} seconds expired",
TIMEOUT
)));
}
let suffix = [(target + counter).to_be_bytes(), counter.to_be_bytes()].concat(); let suffix = [(target + counter).to_be_bytes(), counter.to_be_bytes()].concat();
let mut hasher = Sha1::new(); let mut hasher = Sha1::new();
@ -130,6 +146,8 @@ impl SpClient {
}; };
dst.copy_from_slice(&suffix); dst.copy_from_slice(&suffix);
Ok(())
} }
async fn client_token_request(&self, message: &dyn Message) -> Result<Bytes, Error> { async fn client_token_request(&self, message: &dyn Message) -> Result<Bytes, Error> {
@ -160,10 +178,10 @@ impl SpClient {
trace!("Client token unavailable or expired, requesting new token."); trace!("Client token unavailable or expired, requesting new token.");
let mut message = ClientTokenRequest::new(); let mut request = ClientTokenRequest::new();
message.set_request_type(ClientTokenRequestType::REQUEST_CLIENT_DATA_REQUEST); request.set_request_type(ClientTokenRequestType::REQUEST_CLIENT_DATA_REQUEST);
let client_data = message.mut_client_data(); let client_data = request.mut_client_data();
client_data.set_client_version(spotify_version()); client_data.set_client_version(spotify_version());
// Current state of affairs: keymaster ID works on all tested platforms, but may be phased out, // Current state of affairs: keymaster ID works on all tested platforms, but may be phased out,
@ -238,16 +256,21 @@ impl SpClient {
} }
} }
let mut response = self.client_token_request(&message).await?; let mut response = self.client_token_request(&request).await?;
let mut count = 0;
const MAX_TRIES: u8 = 3;
let token_response = loop { let token_response = loop {
count += 1;
let message = ClientTokenResponse::parse_from_bytes(&response)?; let message = ClientTokenResponse::parse_from_bytes(&response)?;
match ClientTokenResponseType::from_i32(message.response_type.value()) { match ClientTokenResponseType::from_i32(message.response_type.value()) {
// depending on the platform, you're either given a token immediately // depending on the platform, you're either given a token immediately
// or are presented a hash cash challenge to solve first // or are presented a hash cash challenge to solve first
Some(ClientTokenResponseType::RESPONSE_GRANTED_TOKEN_RESPONSE) => break message, Some(ClientTokenResponseType::RESPONSE_GRANTED_TOKEN_RESPONSE) => break message,
Some(ClientTokenResponseType::RESPONSE_CHALLENGES_RESPONSE) => { Some(ClientTokenResponseType::RESPONSE_CHALLENGES_RESPONSE) => {
trace!("received a hash cash challenge, solving..."); trace!("Received a hash cash challenge, solving...");
let challenges = message.get_challenges().clone(); let challenges = message.get_challenges().clone();
let state = challenges.get_state(); let state = challenges.get_state();
@ -257,57 +280,78 @@ impl SpClient {
let ctx = vec![]; let ctx = vec![];
let prefix = hex::decode(&hash_cash_challenge.prefix).map_err(|e| { let prefix = hex::decode(&hash_cash_challenge.prefix).map_err(|e| {
Error::failed_precondition(format!( Error::failed_precondition(format!(
"unable to decode hash cash challenge: {}", "Unable to decode hash cash challenge: {}",
e e
)) ))
})?; })?;
let length = hash_cash_challenge.length; let length = hash_cash_challenge.length;
let mut suffix = vec![0; 0x10]; let mut suffix = vec![0; 0x10];
Self::solve_hash_cash(&ctx, &prefix, length, &mut suffix); let answer = Self::solve_hash_cash(&ctx, &prefix, length, &mut suffix);
// the suffix must be in uppercase match answer {
let suffix = hex::encode(suffix).to_uppercase(); Ok(_) => {
// the suffix must be in uppercase
let suffix = hex::encode(suffix).to_uppercase();
let mut answer_message = ClientTokenRequest::new(); let mut answer_message = ClientTokenRequest::new();
answer_message.set_request_type( answer_message.set_request_type(
ClientTokenRequestType::REQUEST_CHALLENGE_ANSWERS_REQUEST, ClientTokenRequestType::REQUEST_CHALLENGE_ANSWERS_REQUEST,
); );
let challenge_answers = answer_message.mut_challenge_answers(); let challenge_answers = answer_message.mut_challenge_answers();
let mut challenge_answer = ChallengeAnswer::new(); let mut challenge_answer = ChallengeAnswer::new();
challenge_answer.mut_hash_cash().suffix = suffix.to_string(); challenge_answer.mut_hash_cash().suffix = suffix.to_string();
challenge_answer.ChallengeType = ChallengeType::CHALLENGE_HASH_CASH; challenge_answer.ChallengeType = ChallengeType::CHALLENGE_HASH_CASH;
challenge_answers.state = state.to_string(); challenge_answers.state = state.to_string();
challenge_answers.answers.push(challenge_answer); challenge_answers.answers.push(challenge_answer);
trace!("answering hash cash challenge"); trace!("Answering hash cash challenge");
match self.client_token_request(&answer_message).await { match self.client_token_request(&answer_message).await {
Ok(token) => response = token, Ok(token) => {
Err(e) => { response = token;
return Err(Error::failed_precondition(format!( continue;
"unable to solve this challenge: {}", }
e Err(e) => {
))) trace!(
"Answer not accepted {}/{}: {}",
count,
MAX_TRIES,
e
);
}
}
} }
Err(e) => trace!(
"Unable to solve hash cash challenge {}/{}: {}",
count,
MAX_TRIES,
e
),
} }
// we should have been granted a token now if count < MAX_TRIES {
continue; response = self.client_token_request(&request).await?;
} else {
return Err(Error::failed_precondition(format!(
"Unable to solve any of {} hash cash challenges",
MAX_TRIES
)));
}
} else { } else {
return Err(Error::failed_precondition("no challenges found")); return Err(Error::failed_precondition("No challenges found"));
} }
} }
Some(unknown) => { Some(unknown) => {
return Err(Error::unimplemented(format!( return Err(Error::unimplemented(format!(
"unknown client token response type: {:?}", "Unknown client token response type: {:?}",
unknown unknown
))) )))
} }
None => return Err(Error::failed_precondition("no client token response type")), None => return Err(Error::failed_precondition("No client token response type")),
} }
}; };
@ -411,7 +455,6 @@ impl SpClient {
.token_provider() .token_provider()
.get_token("playlist-read") .get_token("playlist-read")
.await?; .await?;
let client_token = self.client_token().await?;
let headers_mut = request.headers_mut(); let headers_mut = request.headers_mut();
if let Some(ref hdrs) = headers { if let Some(ref hdrs) = headers {
@ -421,7 +464,13 @@ impl SpClient {
AUTHORIZATION, AUTHORIZATION,
HeaderValue::from_str(&format!("{} {}", token.token_type, token.access_token,))?, HeaderValue::from_str(&format!("{} {}", token.token_type, token.access_token,))?,
); );
headers_mut.insert(CLIENT_TOKEN, HeaderValue::from_str(&client_token)?);
if let Ok(client_token) = self.client_token().await {
headers_mut.insert(CLIENT_TOKEN, HeaderValue::from_str(&client_token)?);
} else {
// currently these endpoints seem to work fine without it
warn!("Unable to get client token. Trying to continue without...");
}
last_response = self.session().http_client().request_body(request).await; last_response = self.session().http_client().request_body(request).await;