diff --git a/src/authentication.rs b/src/authentication.rs index 5a428508..ce9d70ed 100644 --- a/src/authentication.rs +++ b/src/authentication.rs @@ -1,4 +1,3 @@ -use apresolve::apresolve; use byteorder::{BigEndian, ByteOrder}; use crypto; use crypto::aes; @@ -7,206 +6,58 @@ use crypto::hmac::Hmac; use crypto::mac::Mac; use crypto::pbkdf2::pbkdf2; use crypto::sha1::Sha1; -use protobuf::{self, Message, ProtobufEnum}; -use rand::thread_rng; -use rand::Rng; -use std::io::{self, Read, Write}; -use std::result::Result; +use protobuf::ProtobufEnum; +use std::io::{self, Read}; use rustc_serialize::base64::FromBase64; -use connection::{PlainConnection, CipherConnection}; -use diffie_hellman::DHLocalKeys; -use protocol; use protocol::authentication::AuthenticationType; -use session::Session; -use util; -fn read_u8(stream: &mut R) -> io::Result { - let mut data = [0u8]; - try!(stream.read_exact(&mut data)); - Ok(data[0]) +pub struct Credentials { + pub username: String, + pub auth_type: AuthenticationType, + pub auth_data: Vec, } -fn read_int(stream: &mut R) -> io::Result { - let lo = try!(read_u8(stream)) as u32; - if lo & 0x80 == 0 { - return Ok(lo); - } - - let hi = try!(read_u8(stream)) as u32; - Ok(lo & 0x7f | hi << 7) -} - -fn read_bytes(stream: &mut R) -> io::Result> { - let length = try!(read_int(stream)); - let mut data = vec![0u8; length as usize]; - try!(stream.read_exact(&mut data)); - - Ok(data) -} - -impl Session { - pub fn connect(&self) -> CipherConnection { - let local_keys = DHLocalKeys::random(&mut thread_rng()); - - let aps = apresolve().unwrap(); - let ap = thread_rng().choose(&aps).expect("No APs found"); - - println!("Connecting to AP {}", ap); - let mut connection = PlainConnection::connect(ap).unwrap(); - - let request = protobuf_init!(protocol::keyexchange::ClientHello::new(), { - build_info => { - product: protocol::keyexchange::Product::PRODUCT_LIBSPOTIFY_EMBEDDED, - platform: protocol::keyexchange::Platform::PLATFORM_LINUX_X86, - version: 0x10800000000, - }, - /* - fingerprints_supported => [ - protocol::keyexchange::Fingerprint::FINGERPRINT_GRAIN - ], - */ - cryptosuites_supported => [ - protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON, - //protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_RC4_SHA1_HMAC - ], - /* - powschemes_supported => [ - protocol::keyexchange::Powscheme::POW_HASH_CASH - ], - */ - login_crypto_hello.diffie_hellman => { - gc: local_keys.public_key(), - server_keys_known: 1, - }, - client_nonce: util::rand_vec(&mut thread_rng(), 0x10), - padding: vec![0x1e], - feature_set => { - autoupdate2: true, - } - }); - - let init_client_packet = connection.send_packet_prefix(&[0, 4], - &request.write_to_bytes().unwrap()) - .unwrap(); - let init_server_packet = connection.recv_packet().unwrap(); - - let response: protocol::keyexchange::APResponseMessage = - protobuf::parse_from_bytes(&init_server_packet[4..]).unwrap(); - - let remote_key = response.get_challenge() - .get_login_crypto_challenge() - .get_diffie_hellman() - .get_gs(); - - let shared_secret = local_keys.shared_secret(remote_key); - let (challenge, send_key, recv_key) = { - let mut data = Vec::with_capacity(0x64); - let mut mac = Hmac::new(Sha1::new(), &shared_secret); - - for i in 1..6 { - mac.input(&init_client_packet); - mac.input(&init_server_packet); - mac.input(&[i]); - data.write(&mac.result().code()).unwrap(); - mac.reset(); - } - - mac = Hmac::new(Sha1::new(), &data[..0x14]); - mac.input(&init_client_packet); - mac.input(&init_server_packet); - - (mac.result().code().to_vec(), - data[0x14..0x34].to_vec(), - data[0x34..0x54].to_vec()) - }; - - let packet = protobuf_init!(protocol::keyexchange::ClientResponsePlaintext::new(), { - login_crypto_response.diffie_hellman => { - hmac: challenge - }, - pow_response => {}, - crypto_response => {}, - }); - - - connection.send_packet(&packet.write_to_bytes().unwrap()).unwrap(); - - CipherConnection::new(connection.into_stream(), - &send_key, - &recv_key) - } - - fn login(&self, - username: String, - auth_data: Vec, - typ: AuthenticationType) - -> Result<(), ()> { - - let packet = protobuf_init!(protocol::authentication::ClientResponseEncrypted::new(), { - login_credentials => { - username: username, - typ: typ, - auth_data: auth_data, - }, - system_info => { - cpu_family: protocol::authentication::CpuFamily::CPU_UNKNOWN, - os: protocol::authentication::Os::OS_UNKNOWN, - system_information_string: "librespot".to_owned(), - device_id: self.device_id().clone() - }, - version_string: util::version::version_string(), - appkey => { - version: self.config().application_key[0] as u32, - devkey: self.config().application_key[0x1..0x81].to_vec(), - signature: self.config().application_key[0x81..0x141].to_vec(), - useragent: self.config().user_agent.clone(), - callback_hash: vec![0; 20], - } - }); - - let mut connection = self.connect(); - connection.send_packet(0xab, &packet.write_to_bytes().unwrap()).unwrap(); - let (cmd, data) = connection.recv_packet().unwrap(); - - match cmd { - 0xac => { - let welcome_data: protocol::authentication::APWelcome = - protobuf::parse_from_bytes(&data).unwrap(); - - let username = welcome_data.get_canonical_username().to_owned(); - self.authenticated(username, connection); - - eprintln!("Authenticated !"); - Ok(()) - } - - 0xad => { - let msg: protocol::keyexchange::APLoginFailed = protobuf::parse_from_bytes(&data) - .unwrap(); - eprintln!("Authentication failed, {:?}", msg); - Err(()) - } - _ => { - println!("Unexpected message {:x}", cmd); - Err(()) - } +impl Credentials { + pub fn with_password(username: String, password: String) -> Credentials { + Credentials { + username: username, + auth_type: AuthenticationType::AUTHENTICATION_USER_PASS, + auth_data: password.into_bytes(), } } - pub fn login_password(&self, username: String, password: String) -> Result<(), ()> { - self.login(username, - password.into_bytes(), - AuthenticationType::AUTHENTICATION_USER_PASS) - } + pub fn with_blob(username: String, encrypted_blob: &str, device_id: &str) -> Credentials { + fn read_u8(stream: &mut R) -> io::Result { + let mut data = [0u8]; + try!(stream.read_exact(&mut data)); + Ok(data[0]) + } - pub fn login_blob(&self, username: String, blob: &str) -> Result<(), ()> { - let blob = blob.from_base64().unwrap(); + fn read_int(stream: &mut R) -> io::Result { + let lo = try!(read_u8(stream)) as u32; + if lo & 0x80 == 0 { + return Ok(lo); + } + + let hi = try!(read_u8(stream)) as u32; + Ok(lo & 0x7f | hi << 7) + } + + fn read_bytes(stream: &mut R) -> io::Result> { + let length = try!(read_int(stream)); + let mut data = vec![0u8; length as usize]; + try!(stream.read_exact(&mut data)); + + Ok(data) + } + + let encrypted_blob = encrypted_blob.from_base64().unwrap(); let secret = { let mut data = [0u8; 20]; let mut h = crypto::sha1::Sha1::new(); - h.input(&self.device_id().as_bytes()); + h.input(device_id.as_bytes()); h.result(&mut data); data }; @@ -225,16 +76,16 @@ impl Session { let blob = { // Anyone know what this block mode is ? - let mut data = vec![0u8; blob.len()]; + let mut data = vec![0u8; encrypted_blob.len()]; let mut cipher = aes::ecb_decryptor(aes::KeySize::KeySize192, &key, crypto::blockmodes::NoPadding); - cipher.decrypt(&mut crypto::buffer::RefReadBuffer::new(&blob), + cipher.decrypt(&mut crypto::buffer::RefReadBuffer::new(&encrypted_blob), &mut crypto::buffer::RefWriteBuffer::new(&mut data), true) .unwrap(); - let l = blob.len(); + let l = encrypted_blob.len(); for i in 0..l - 0x10 { data[l - i - 1] ^= data[l - i - 0x11]; } @@ -251,6 +102,10 @@ impl Session { read_u8(&mut cursor).unwrap(); let auth_data = read_bytes(&mut cursor).unwrap();; - self.login(username, auth_data, auth_type) + Credentials { + username: username, + auth_type: auth_type, + auth_data: auth_data, + } } } diff --git a/src/discovery.rs b/src/discovery.rs index 4103675c..a8794628 100644 --- a/src/discovery.rs +++ b/src/discovery.rs @@ -5,9 +5,10 @@ use num::BigUint; use url; use rand; use rustc_serialize::base64::{self, ToBase64, FromBase64}; -use tiny_http::{Method, Response, ResponseBox, Server}; +use tiny_http::{Method, Request, Response, ResponseBox, Server}; use zeroconf::DNSService; +use authentication::Credentials; use session::Session; use diffie_hellman::{DH_GENERATOR, DH_PRIME}; use util; @@ -54,7 +55,7 @@ impl DiscoveryManager { }).to_string()).boxed() } - fn add_user(&self, params: &[(String, String)]) -> ResponseBox { + fn add_user(&self, params: &[(String, String)]) -> (ResponseBox, Credentials) { let &(_, ref username) = params.iter().find(|&&(ref key, _)| key == "userName").unwrap(); let &(_, ref encrypted_blob) = params.iter().find(|&&(ref key, _)| key == "blob").unwrap(); let &(_, ref client_key) = params.iter().find(|&&(ref key, _)| key == "clientKey").unwrap(); @@ -107,49 +108,60 @@ impl DiscoveryManager { String::from_utf8(data).unwrap() }; - self.session.login_blob(username.to_owned(), &decrypted).unwrap(); - - Response::from_string(json!({ + let response = Response::from_string(json!({ "status": 101, "spotifyError": 0, "statusString": "ERROR-OK" - }).to_string()).boxed() + }).to_string()).boxed(); + + let credentials = Credentials::with_blob(username.to_owned(), &decrypted, &self.session.device_id()); + + (response, credentials) } - pub fn run(&mut self) { - let server = Server::http("0.0.0.0:8000").unwrap(); - let svc = DNSService::register(Some(&self.session.config().device_name), - "_spotify-connect._tcp", - None, - None, - 8000, - &["VERSION=1.0", "CPath=/"] - ).unwrap(); + fn handle_request(&self, mut request: Request) -> Option { + let (_, query, _) = url::parse_path(request.url()).unwrap(); + let mut params = query.map_or(vec![], |q| url::form_urlencoded::parse(q.as_bytes())); - for mut request in server.incoming_requests() { - let (_, query, _) = url::parse_path(request.url()).unwrap(); - let mut params = query.map_or(vec![], |q| url::form_urlencoded::parse(q.as_bytes())); - - if *request.method() == Method::Post { - let mut body = Vec::new(); - request.as_reader().read_to_end(&mut body).unwrap(); - let form = url::form_urlencoded::parse(&body); - params.extend(form); - } - - println!("{:?}", params); - - let &(_, ref action) = params.iter().find(|&&(ref key, _)| key == "action").unwrap(); - match action.as_ref() { - "getInfo" => request.respond(self.get_info()).unwrap(), - "addUser" => { - request.respond(self.add_user(¶ms)).unwrap(); - break; - } - _ => request.respond(not_found()).unwrap(), - }; + if *request.method() == Method::Post { + let mut body = Vec::new(); + request.as_reader().read_to_end(&mut body).unwrap(); + let form = url::form_urlencoded::parse(&body); + params.extend(form); } - drop(svc); + println!("{:?}", params); + + let &(_, ref action) = params.iter().find(|&&(ref key, _)| key == "action").unwrap(); + let (response, credentials) = match action.as_ref() { + "getInfo" => (self.get_info(), None), + "addUser" => { + let (response, credentials) = self.add_user(¶ms); + (response, Some(credentials)) + } + _ => (not_found(), None) + }; + + request.respond(response).unwrap(); + credentials + } + + pub fn run(&mut self) -> Credentials { + let server = Server::http("0.0.0.0:8000").unwrap(); + let _svc = DNSService::register(Some(&self.session.config().device_name), + "_spotify-connect._tcp", + None, + None, + 8000, + &["VERSION=1.0", "CPath=/"] + ).unwrap(); + + for request in server.incoming_requests() { + if let Some(credentials) = self.handle_request(request) { + return credentials; + } + } + + panic!("No credentials obtained !"); } } diff --git a/src/lib.in.rs b/src/lib.in.rs index 34320a01..9c01b6ff 100644 --- a/src/lib.in.rs +++ b/src/lib.in.rs @@ -2,7 +2,7 @@ mod audio_decrypt; mod audio_file; mod audio_key; -mod authentication; +pub mod authentication; mod connection; mod diffie_hellman; pub mod discovery; diff --git a/src/main.rs b/src/main.rs index 8bdf2a6b..4eafa1e8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ use std::io::{stdout, Read, Write}; use std::path::{Path, PathBuf}; use std::thread; +use librespot::authentication::Credentials; use librespot::discovery::DiscoveryManager; use librespot::player::Player; use librespot::session::{Bitrate, Config, Session}; @@ -89,12 +90,14 @@ fn main() { let session = Session::new(config); - if let Some((username, password)) = credentials { - session.login_password(username, password).unwrap(); - } else { + let credentials = credentials.map_or_else(|| { let mut discovery = DiscoveryManager::new(session.clone()); - discovery.run(); - } + discovery.run() + }, |(username, password)| { + Credentials::with_password(username, password) + }); + + session.login(credentials).unwrap(); let player = Player::new(session.clone()); let mut spirc = SpircManager::new(session.clone(), player); diff --git a/src/session.rs b/src/session.rs index fb762345..37dab6fd 100644 --- a/src/session.rs +++ b/src/session.rs @@ -1,18 +1,28 @@ use crypto::digest::Digest; use crypto::sha1::Sha1; +use crypto::hmac::Hmac; +use crypto::mac::Mac; use eventual::Future; -use std::io::Write; +use protobuf::{self, Message}; +use rand::thread_rng; +use rand::Rng; +use std::io::{Read, Write}; use std::path::PathBuf; +use std::result::Result; use std::sync::{Mutex, RwLock, Arc, mpsc}; +use apresolve::apresolve; use audio_key::{AudioKeyManager, AudioKey, AudioKeyError}; use audio_file::{AudioFileManager, AudioFile}; -use connection::{self, CipherConnection}; -use connection::PacketHandler; +use authentication::Credentials; +use connection::{self, PlainConnection, CipherConnection, PacketHandler}; +use diffie_hellman::DHLocalKeys; use mercury::{MercuryManager, MercuryRequest, MercuryResponse}; use metadata::{MetadataManager, MetadataRef, MetadataTrait}; +use protocol; use stream::{StreamManager, StreamEvent}; -use util::{SpotifyId, FileId, mkdir_existing}; +use util::{self, SpotifyId, FileId, mkdir_existing}; + pub enum Bitrate { Bitrate96, @@ -79,10 +89,149 @@ impl Session { })) } - pub fn authenticated(&self, username: String, connection: CipherConnection) { - self.0.data.write().unwrap().canonical_username = username; - *self.0.rx_connection.lock().unwrap() = Some(connection.clone()); - *self.0.tx_connection.lock().unwrap() = Some(connection); + fn connect(&self) -> CipherConnection { + let local_keys = DHLocalKeys::random(&mut thread_rng()); + + let aps = apresolve().unwrap(); + let ap = thread_rng().choose(&aps).expect("No APs found"); + + println!("Connecting to AP {}", ap); + let mut connection = PlainConnection::connect(ap).unwrap(); + + let request = protobuf_init!(protocol::keyexchange::ClientHello::new(), { + build_info => { + product: protocol::keyexchange::Product::PRODUCT_LIBSPOTIFY_EMBEDDED, + platform: protocol::keyexchange::Platform::PLATFORM_LINUX_X86, + version: 0x10800000000, + }, + /* + fingerprints_supported => [ + protocol::keyexchange::Fingerprint::FINGERPRINT_GRAIN + ], + */ + cryptosuites_supported => [ + protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_SHANNON, + //protocol::keyexchange::Cryptosuite::CRYPTO_SUITE_RC4_SHA1_HMAC + ], + /* + powschemes_supported => [ + protocol::keyexchange::Powscheme::POW_HASH_CASH + ], + */ + login_crypto_hello.diffie_hellman => { + gc: local_keys.public_key(), + server_keys_known: 1, + }, + client_nonce: util::rand_vec(&mut thread_rng(), 0x10), + padding: vec![0x1e], + feature_set => { + autoupdate2: true, + } + }); + + let init_client_packet = connection.send_packet_prefix(&[0, 4], + &request.write_to_bytes().unwrap()) + .unwrap(); + let init_server_packet = connection.recv_packet().unwrap(); + + let response: protocol::keyexchange::APResponseMessage = + protobuf::parse_from_bytes(&init_server_packet[4..]).unwrap(); + + let remote_key = response.get_challenge() + .get_login_crypto_challenge() + .get_diffie_hellman() + .get_gs(); + + let shared_secret = local_keys.shared_secret(remote_key); + let (challenge, send_key, recv_key) = { + let mut data = Vec::with_capacity(0x64); + let mut mac = Hmac::new(Sha1::new(), &shared_secret); + + for i in 1..6 { + mac.input(&init_client_packet); + mac.input(&init_server_packet); + mac.input(&[i]); + data.write(&mac.result().code()).unwrap(); + mac.reset(); + } + + mac = Hmac::new(Sha1::new(), &data[..0x14]); + mac.input(&init_client_packet); + mac.input(&init_server_packet); + + (mac.result().code().to_vec(), + data[0x14..0x34].to_vec(), + data[0x34..0x54].to_vec()) + }; + + let packet = protobuf_init!(protocol::keyexchange::ClientResponsePlaintext::new(), { + login_crypto_response.diffie_hellman => { + hmac: challenge + }, + pow_response => {}, + crypto_response => {}, + }); + + + connection.send_packet(&packet.write_to_bytes().unwrap()).unwrap(); + + CipherConnection::new(connection.into_stream(), + &send_key, + &recv_key) + } + + pub fn login(&self, credentials: Credentials) -> Result<(), ()> { + let packet = protobuf_init!(protocol::authentication::ClientResponseEncrypted::new(), { + login_credentials => { + username: credentials.username, + typ: credentials.auth_type, + auth_data: credentials.auth_data, + }, + system_info => { + cpu_family: protocol::authentication::CpuFamily::CPU_UNKNOWN, + os: protocol::authentication::Os::OS_UNKNOWN, + system_information_string: "librespot".to_owned(), + device_id: self.device_id(), + }, + version_string: util::version::version_string(), + appkey => { + version: self.config().application_key[0] as u32, + devkey: self.config().application_key[0x1..0x81].to_vec(), + signature: self.config().application_key[0x81..0x141].to_vec(), + useragent: self.config().user_agent.clone(), + callback_hash: vec![0; 20], + } + }); + + let mut connection = self.connect(); + connection.send_packet(0xab, &packet.write_to_bytes().unwrap()).unwrap(); + let (cmd, data) = connection.recv_packet().unwrap(); + + match cmd { + 0xac => { + let welcome_data: protocol::authentication::APWelcome = + protobuf::parse_from_bytes(&data).unwrap(); + + let username = welcome_data.get_canonical_username().to_owned(); + self.0.data.write().unwrap().canonical_username = username; + *self.0.rx_connection.lock().unwrap() = Some(connection.clone()); + *self.0.tx_connection.lock().unwrap() = Some(connection); + + eprintln!("Authenticated !"); + Ok(()) + } + + 0xad => { + let msg: protocol::keyexchange::APLoginFailed = + protobuf::parse_from_bytes(&data).unwrap(); + eprintln!("Authentication failed, {:?}", msg); + Err(()) + } + _ => { + println!("Unexpected message {:x}", cmd); + Err(()) + } + } } pub fn poll(&self) {