From bead9daf78a5100cf1ef68b87795009ec2969b60 Mon Sep 17 00:00:00 2001 From: Paul Lietar Date: Sat, 2 Jan 2016 00:16:12 +0100 Subject: [PATCH] Add zeroconf based authentication. --- .travis.yml | 1 + Cargo.lock | 147 +++++++++++++++++++++++++++++++++++++- Cargo.toml | 7 +- protocol/Cargo.toml | 2 +- src/authentication.rs | 149 +++++++++++++++++++++++++++++++++++++++ src/connection.rs | 22 +++--- src/diffie_hellman.rs | 51 ++++++++++++++ src/discovery.rs | 154 ++++++++++++++++++++++++++++++++++++++++ src/keys.rs | 103 --------------------------- src/lib.rs | 21 ++++-- src/main.rs | 50 ++++++------- src/player.rs | 13 ++-- src/session.rs | 160 +++++++++++++++++++++--------------------- src/spirc.rs | 33 ++++++--- 14 files changed, 664 insertions(+), 249 deletions(-) create mode 100644 src/authentication.rs create mode 100644 src/diffie_hellman.rs create mode 100644 src/discovery.rs delete mode 100644 src/keys.rs diff --git a/.travis.yml b/.travis.yml index ac353d33..d82a66c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ addons: packages: - protobuf-compiler - portaudio19-dev + - libavahi-compat-libdnssd-dev before_install: - export PATH=$HOME/.cargo/bin:$PATH diff --git a/Cargo.lock b/Cargo.lock index cc045d45..53a8ab0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,20 +4,25 @@ version = "0.1.0" dependencies = [ "bit-set 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "dns-sd 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "eventual 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "getopts 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", + "json_macros 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", "librespot-protocol 0.1.0", "num 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", "portaudio 0.1.2 (git+https://github.com/mvdnes/portaudio-rs)", - "protobuf 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "protobuf 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", "protobuf_macros 0.1.0 (git+https://github.com/plietar/rust-protobuf-macros.git)", "rand 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", "rpassword 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "rust-crypto 0.2.34 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", "shannon 0.1.1 (git+https://github.com/plietar/rust-shannon.git)", "tempfile 1.1.3 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)", + "tiny_http 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "url 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", "vergen 0.0.16 (registry+https://github.com/rust-lang/crates.io-index)", "vorbis 0.0.13 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -31,6 +36,11 @@ dependencies = [ "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ascii" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "bit-set" version = "0.2.0" @@ -54,6 +64,82 @@ name = "byteorder" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "chrono" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "num 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.34 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "chunked_transfer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "dns-sd" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "encoding" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "encoding-index-japanese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding-index-korean 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding-index-simpchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding-index-singlebyte 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding-index-tradchinese 1.20141219.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "encoding-index-japanese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "encoding-index-korean" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "encoding-index-simpchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "encoding-index-singlebyte" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "encoding-index-tradchinese" +version = "1.20141219.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "encoding_index_tests 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "encoding_index_tests" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "eventual" version = "0.1.5" @@ -78,6 +164,14 @@ name = "getopts" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "json_macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rustc-serialize 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "kernel32-sys" version = "0.1.4" @@ -116,7 +210,7 @@ name = "librespot-protocol" version = "0.1.0" dependencies = [ "mod_path 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", - "protobuf 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "protobuf 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -127,6 +221,11 @@ dependencies = [ "libc 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "matches" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "mod_path" version = "0.1.6" @@ -177,7 +276,7 @@ dependencies = [ [[package]] name = "protobuf" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -278,6 +377,48 @@ dependencies = [ "winapi 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tiny_http" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ascii 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)", + "chunked_transfer 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "encoding 0.2.32 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", + "url 0.2.38 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "url" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "url" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "uuid" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.3.12 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "vergen" version = "0.0.16" diff --git a/Cargo.toml b/Cargo.toml index ae10d833..0a4596f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,16 +18,21 @@ path = "protocol" [dependencies] bit-set = "~0.2.0" byteorder = "~0.4.2" +dns-sd = "~0.1.0" eventual = "~0.1.5" getopts = "~0.2.14" +json_macros = "~0.2.6" lazy_static = "~0.1.15" num = "~0.1.29" -protobuf = "~1.0.9" +protobuf = "~1.0.10" rand = "~0.3.12" rpassword = "~0.1.0" rust-crypto = "~0.2.34" +rustc-serialize = "~0.3.16" time = "~0.1.34" +tiny_http = "~0.5.1" tempfile = "~1.1.3" +url = "~0.5.2" vorbis = "~0.0.13" [dependencies.protobuf_macros] diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 0a078eb7..0bed13fa 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -6,5 +6,5 @@ build = "build.rs" [dependencies] mod_path = "~0.1.6" -protobuf = "~1.0.4" +protobuf = "~1.0.10" diff --git a/src/authentication.rs b/src/authentication.rs new file mode 100644 index 00000000..dddfd3d8 --- /dev/null +++ b/src/authentication.rs @@ -0,0 +1,149 @@ +use byteorder::{BigEndian, ByteOrder}; +use crypto; +use crypto::aes; +use crypto::pbkdf2::pbkdf2; +use crypto::sha1::Sha1; +use crypto::hmac::Hmac; +use crypto::digest::Digest; +use protobuf::{self, Message, ProtobufEnum}; +use std::io::{self, Read}; +use std::result::Result; +use rustc_serialize::base64::FromBase64; + +use librespot_protocol as protocol; +use librespot_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]) +} + +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 { + 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.0.data.read().unwrap().device_id.clone() + }, + version_string: util::version::version_string(), + appkey => { + version: self.0.config.application_key[0] as u32, + devkey: self.0.config.application_key[0x1..0x81].to_vec(), + signature: self.0.config.application_key[0x81..0x141].to_vec(), + useragent: self.0.config.user_agent.clone(), + callback_hash: vec![0; 20], + } + }); + + self.connect(); + self.send_packet(0xab, &packet.write_to_bytes().unwrap()).unwrap(); + let (cmd, data) = self.recv(); + match cmd { + 0xac => { + let welcome_data : protocol::authentication::APWelcome = + protobuf::parse_from_bytes(&data).unwrap(); + self.0.data.write().unwrap().canonical_username = + welcome_data.get_canonical_username().to_string(); + + 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 login_password(&self, username: String, password: String) -> Result<(), ()> { + self.login(username, password.into_bytes(), + AuthenticationType::AUTHENTICATION_USER_PASS) + } + + pub fn login_blob(&self, username: String, blob: &str) -> Result<(), ()> { + let blob = blob.from_base64().unwrap(); + + let secret = { + let mut data = [0u8; 20]; + let mut h = crypto::sha1::Sha1::new(); + h.input(&self.0.data.read().unwrap().device_id.as_bytes()); + h.result(&mut data); + data + }; + + let key = { + let mut data = [0u8; 24]; + let mut mac = Hmac::new(Sha1::new(), &secret); + pbkdf2(&mut mac, username.as_bytes(), 0x100, &mut data[0..20]); + + let mut hash = Sha1::new(); + hash.input(&data[0..20]); + hash.result(&mut data[0..20]); + BigEndian::write_u32(&mut data[20..], 20); + data + }; + + let blob = { + // Anyone know what this block mode is ? + let mut data = vec![0u8; blob.len()]; + let mut cipher = aes::ecb_decryptor( + aes::KeySize::KeySize192, &key, crypto::blockmodes::NoPadding); + cipher.decrypt(&mut crypto::buffer::RefReadBuffer::new(&blob), + &mut crypto::buffer::RefWriteBuffer::new(&mut data), + true).unwrap(); + + let l = blob.len(); + for i in 0..l-0x10 { + data[l - i - 1] ^= data[l - i - 0x11]; + } + + data + }; + + let mut cursor = io::Cursor::new(&blob); + read_u8(&mut cursor).unwrap(); + read_bytes(&mut cursor).unwrap(); + read_u8(&mut cursor).unwrap(); + let auth_type = read_int(&mut cursor).unwrap(); + let auth_type = AuthenticationType::from_i32(auth_type as i32).unwrap(); + read_u8(&mut cursor).unwrap(); + let auth_data = read_bytes(&mut cursor).unwrap();; + + self.login(username, auth_data, auth_type) + } +} diff --git a/src/connection.rs b/src/connection.rs index b299efdf..c77d22f5 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -6,8 +6,6 @@ use std::io::{Read, Write}; use std::net::TcpStream; use std::result; -use keys::SharedKeys; - #[derive(Debug)] pub enum Error { IoError(io::Error), @@ -74,14 +72,18 @@ impl PlainConnection { Ok(buffer) } - pub fn setup_cipher(self, keys: SharedKeys) -> CipherConnection { - CipherConnection{ - stream: ShannonStream::new(self.stream, &keys.send_key(), &keys.recv_key()) - } + pub fn into_stream(self) -> TcpStream { + self.stream } } impl CipherConnection { + pub fn new(stream: TcpStream, recv_key: &[u8], send_key: &[u8]) -> CipherConnection { + CipherConnection { + stream: ShannonStream::new(stream, recv_key, send_key) + } + } + pub fn send_packet(&mut self, cmd: u8, data: &[u8]) -> Result<()> { try!(self.stream.write_u8(cmd)); try!(self.stream.write_u16::(data.len() as u16)); try!(self.stream.write(data)); @@ -109,11 +111,3 @@ pub trait PacketHandler { fn handle(&mut self, cmd: u8, data: Vec); } -/* - match packet.cmd { - 0x09 => &self.dispatch.stream, - 0xd | 0xe => &self.dispatch.audio_key, - 0xb2...0xb6 => &self.dispatch.mercury, - _ => &self.dispatch.main, - }.send(packet).unwrap(); - */ diff --git a/src/diffie_hellman.rs b/src/diffie_hellman.rs new file mode 100644 index 00000000..f9e8c906 --- /dev/null +++ b/src/diffie_hellman.rs @@ -0,0 +1,51 @@ +use num::{BigUint, FromPrimitive}; +use rand::Rng; + +use util; + +lazy_static! { + pub static ref DH_GENERATOR: BigUint = BigUint::from_u64(0x2).unwrap(); + pub static ref DH_PRIME: BigUint = BigUint::from_bytes_be(&[ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, + 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, 0xc4, 0xc6, + 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, + 0x08, 0x8a, 0x67, 0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6, + 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, + 0x34, 0x04, 0xdd, 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, + 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, + 0x37, 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, + 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, 0xf4, + 0x4c, 0x42, 0xe9, 0xa6, 0x3a, 0x36, 0x20, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff ]); +} + +pub struct DHLocalKeys { + private_key: BigUint, + public_key: BigUint, +} + +impl DHLocalKeys { + pub fn random(rng: &mut R) -> DHLocalKeys { + let key_data = util::rand_vec(rng, 95); + + let private_key = BigUint::from_bytes_be(&key_data); + let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME); + + DHLocalKeys { + private_key: private_key, + public_key: public_key, + } + } + + pub fn public_key(&self) -> Vec { + return self.public_key.to_bytes_be(); + } + + pub fn shared_secret(&self, remote_key: &[u8]) -> Vec { + let shared_key = util::powm(&BigUint::from_bytes_be(remote_key), + &self.private_key, + &DH_PRIME); + shared_key.to_bytes_be() + } +} + diff --git a/src/discovery.rs b/src/discovery.rs new file mode 100644 index 00000000..c3fc8e3e --- /dev/null +++ b/src/discovery.rs @@ -0,0 +1,154 @@ +use crypto; +use crypto::mac::Mac; +use crypto::digest::Digest; +use dns_sd::DNSService; +use tiny_http::{Method, Response, ResponseBox, Server}; +use num::BigUint; +use url; +use rand; +use rustc_serialize::base64::{self, ToBase64, FromBase64}; + +use session::Session; +use diffie_hellman::{DH_GENERATOR, DH_PRIME}; +use util; + +pub struct DiscoveryManager { + session: Session, + private_key: BigUint, + public_key: BigUint +} + +fn not_found() -> ResponseBox { + Response::from_string("Not found").with_status_code(404).boxed() +} + +impl DiscoveryManager { + pub fn new(session: Session) -> DiscoveryManager { + let key_data = util::rand_vec(&mut rand::thread_rng(), 95); + let private_key = BigUint::from_bytes_be(&key_data); + let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME); + + DiscoveryManager { + session: session, + private_key: private_key, + public_key: public_key, + } + } + + fn get_info(&self) -> ResponseBox { + let public_key = self.public_key.to_bytes_be().to_base64(base64::STANDARD); + Response::from_string(json!({ + "status": 101, + "statusString": "ERROR-OK", + "spotifyError": 0, + "version": "2.1.0", + "deviceID": (self.session.0.data.read().unwrap().device_id), + "remoteName": (self.session.0.config.device_name), + "activeUser": "", + "publicKey": (public_key), + "deviceType": "UNKNOWN", + "libraryVersion": "0.1.0", + "accountReq": "PREMIUM", + "brandDisplayName": "librespot", + "modelDisplayName": "librespot", + }).to_string()).boxed() + } + + fn add_user(&self, params: &[(String, String)]) -> ResponseBox { + 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(); + + let encrypted_blob = encrypted_blob.from_base64().unwrap(); + + let client_key = client_key.from_base64().unwrap(); + let client_key = BigUint::from_bytes_be(&client_key); + + let shared_key = util::powm(&client_key, &self.private_key, &DH_PRIME); + + let iv = &encrypted_blob[0..16]; + let encrypted = &encrypted_blob[16..encrypted_blob.len()-20]; + let cksum = &encrypted_blob[encrypted_blob.len()-20..encrypted_blob.len()]; + + let base_key = { + let mut data = [0u8; 20]; + let mut h = crypto::sha1::Sha1::new(); + h.input(&shared_key.to_bytes_be()); + h.result(&mut data); + data[..16].to_owned() + }; + + let checksum_key = { + let mut h = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &base_key); + h.input("checksum".as_bytes()); + h.result().code().to_owned() + }; + + let encryption_key = { + let mut h = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &base_key); + h.input("encryption".as_bytes()); + h.result().code().to_owned() + }; + + let mac = { + let mut h = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &checksum_key); + h.input(encrypted); + h.result().code().to_owned() + }; + + assert_eq!(mac.as_slice(), cksum); + + let decrypted = { + let mut data = vec![0u8; encrypted.len()]; + let mut cipher = crypto::aes::ctr(crypto::aes::KeySize::KeySize128, &encryption_key, &iv); + cipher.process(&encrypted, &mut data); + String::from_utf8(data).unwrap() + }; + + self.session.login_blob(username.to_owned(), &decrypted).unwrap(); + + Response::from_string(json!({ + "status": 101, + "spotifyError": 0, + "statusString": "ERROR-OK" + }).to_string()).boxed() + } + + pub fn run(&mut self) { + let server = Server::http("0.0.0.0:8000").unwrap(); + let svc = DNSService::register(Some(&self.session.0.config.device_name), + "_spotify-connect._tcp", + None, + None, + 8000, + &["VERSION=1.0", "CPath=/"] + ).unwrap(); + + for mut request in server.incoming_requests() { + let (_, query, _) = url::parse_path(request.url()).unwrap(); + let mut params = query.map(|q| url::form_urlencoded::parse(q.as_bytes())).unwrap_or(Vec::new()); + + 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(), + }; + } + + drop(svc); + } +} + diff --git a/src/keys.rs b/src/keys.rs deleted file mode 100644 index da8eb836..00000000 --- a/src/keys.rs +++ /dev/null @@ -1,103 +0,0 @@ -use crypto; -use crypto::mac::Mac; -use num::{BigUint, FromPrimitive}; -use rand; -use std::io::Write; - -use util; - -lazy_static! { - static ref DH_GENERATOR: BigUint = BigUint::from_u64(0x2).unwrap(); - static ref DH_PRIME: BigUint = BigUint::from_bytes_be(&[ - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xc9, - 0x0f, 0xda, 0xa2, 0x21, 0x68, 0xc2, 0x34, 0xc4, 0xc6, - 0x62, 0x8b, 0x80, 0xdc, 0x1c, 0xd1, 0x29, 0x02, 0x4e, - 0x08, 0x8a, 0x67, 0xcc, 0x74, 0x02, 0x0b, 0xbe, 0xa6, - 0x3b, 0x13, 0x9b, 0x22, 0x51, 0x4a, 0x08, 0x79, 0x8e, - 0x34, 0x04, 0xdd, 0xef, 0x95, 0x19, 0xb3, 0xcd, 0x3a, - 0x43, 0x1b, 0x30, 0x2b, 0x0a, 0x6d, 0xf2, 0x5f, 0x14, - 0x37, 0x4f, 0xe1, 0x35, 0x6d, 0x6d, 0x51, 0xc2, 0x45, - 0xe4, 0x85, 0xb5, 0x76, 0x62, 0x5e, 0x7e, 0xc6, 0xf4, - 0x4c, 0x42, 0xe9, 0xa6, 0x3a, 0x36, 0x20, 0xff, 0xff, - 0xff, 0xff, 0xff, 0xff, 0xff, 0xff ]); -} - -pub struct PrivateKeys { - private_key: BigUint, - public_key: BigUint, -} - -pub struct SharedKeys { - //private: PrivateKeys, - challenge: Vec, - send_key: Vec, - recv_key: Vec -} - -impl PrivateKeys { - pub fn new() -> PrivateKeys { - let key_data = util::rand_vec(&mut rand::thread_rng(), 95); - Self::new_with_key(&key_data) - } - - pub fn new_with_key(key_data: &[u8]) -> PrivateKeys { - let private_key = BigUint::from_bytes_be(key_data); - let public_key = util::powm(&DH_GENERATOR, &private_key, &DH_PRIME); - - PrivateKeys { - private_key: private_key, - public_key: public_key, - } - } - - /* - pub fn private_key(&self) -> Vec { - return self.private_key.to_bytes_be(); - } - */ - - pub fn public_key(&self) -> Vec { - return self.public_key.to_bytes_be(); - } - - pub fn add_remote_key(self, remote_key: &[u8], client_packet: &[u8], server_packet: &[u8]) -> SharedKeys { - let shared_key = util::powm(&BigUint::from_bytes_be(remote_key), &self.private_key, &DH_PRIME); - - let mut data = Vec::with_capacity(0x64); - let mut mac = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &shared_key.to_bytes_be()); - - for i in 1..6 { - mac.input(client_packet); - mac.input(server_packet); - mac.input(&[i]); - data.write(&mac.result().code()).unwrap(); - mac.reset(); - } - - mac = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &data[..0x14]); - mac.input(client_packet); - mac.input(server_packet); - - SharedKeys { - //private: self, - challenge: mac.result().code().to_vec(), - send_key: data[0x14..0x34].to_vec(), - recv_key: data[0x34..0x54].to_vec(), - } - } -} - -impl SharedKeys { - pub fn challenge(&self) -> &[u8] { - &self.challenge - } - - pub fn send_key(&self) -> &[u8] { - &self.send_key - } - - pub fn recv_key(&self) -> &[u8] { - &self.recv_key - } -} - diff --git a/src/lib.rs b/src/lib.rs index 3dd25f8f..8710f0f3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,35 +1,42 @@ #![crate_name = "librespot"] -#![feature(plugin,zero_one,iter_arith,mpsc_select,clone_from_slice)] +#![feature(plugin,zero_one,iter_arith,mpsc_select,clone_from_slice,convert)] #![plugin(protobuf_macros)] +#![plugin(json_macros)] #[macro_use] extern crate lazy_static; extern crate bit_set; extern crate byteorder; extern crate crypto; +extern crate dns_sd; extern crate eventual; extern crate num; extern crate portaudio; extern crate protobuf; extern crate shannon; extern crate rand; +extern crate rustc_serialize; extern crate time; +extern crate tiny_http; extern crate tempfile; +extern crate url; extern crate vorbis; extern crate librespot_protocol; #[macro_use] pub mod util; -pub mod audio_decrypt; -pub mod audio_file; -pub mod audio_key; -pub mod connection; -pub mod keys; +mod audio_decrypt; +mod audio_file; +mod audio_key; +mod authentication; +mod connection; +mod diffie_hellman; +pub mod discovery; pub mod mercury; pub mod metadata; pub mod player; pub mod session; pub mod spirc; -pub mod stream; +mod stream; diff --git a/src/main.rs b/src/main.rs index e3429792..e4292bfc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,20 +2,19 @@ extern crate getopts; extern crate librespot; extern crate rpassword; +use getopts::Options; +use rpassword::read_password; use std::clone::Clone; use std::fs::File; use std::io::{stdout, Read, Write}; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::thread; -use std::path::PathBuf; -use getopts::Options; -use rpassword::read_password; - -use librespot::session::{Config, Session}; -use librespot::util::version::version_string; +use librespot::discovery::DiscoveryManager; use librespot::player::Player; +use librespot::session::{Config, Session}; use librespot::spirc::SpircManager; +use librespot::util::version::version_string; fn usage(program: &str, opts: &Options) -> String { let brief = format!("Usage: {} [options]", program); @@ -40,9 +39,16 @@ fn main() { } }; - let mut appkey_file = File::open( - Path::new(&*matches.opt_str("a").unwrap()) - ).expect("Could not open app key."); + let appkey = { + let mut file = File::open( + Path::new(&*matches.opt_str("a").unwrap()) + ).expect("Could not open app key."); + + let mut data = Vec::new(); + file.read_to_end(&mut data).unwrap(); + + data + }; let username = matches.opt_str("u").unwrap(); let cache_location = matches.opt_str("c").unwrap(); @@ -54,30 +60,26 @@ fn main() { read_password().unwrap() }); - let mut appkey = Vec::new(); - appkey_file.read_to_end(&mut appkey).unwrap(); - let config = Config { application_key: appkey, user_agent: version_string(), - device_id: name.clone(), + device_name: name, cache_location: PathBuf::from(cache_location) }; let session = Session::new(config); - session.login(username.clone(), password); - session.poll(); + //session.login_password(username, password).unwrap(); + let mut discovery = DiscoveryManager::new(session.clone()); + discovery.run(); - let _session = session.clone(); + let player = Player::new(session.clone()); + let mut spirc = SpircManager::new(session.clone(), player); thread::spawn(move || { - loop { - _session.poll(); - } + spirc.run() }); - let player = Player::new(&session); - - let mut spirc_manager = SpircManager::new(&session, player, name); - spirc_manager.run(); + loop { + session.poll(); + } } diff --git a/src/player.rs b/src/player.rs index 935d866e..b5fcbcaf 100644 --- a/src/player.rs +++ b/src/player.rs @@ -40,7 +40,7 @@ enum PlayerCommand { } impl Player { - pub fn new(session: &Session) -> Player { + pub fn new(session: Session) -> Player { let (cmd_tx, cmd_rx) = mpsc::channel(); let state = Arc::new((Mutex::new(PlayerState { @@ -52,7 +52,7 @@ impl Player { }), Condvar::new())); let internal = PlayerInternal { - session: session.clone(), + session: session, commands: cmd_rx, state: state.clone() }; @@ -85,7 +85,8 @@ impl PlayerInternal { let mut decoder = None; loop { - let cmd = if self.state.0.lock().unwrap().status == PlayStatus::kPlayStatusPlay { + let playing = self.state.0.lock().unwrap().status == PlayStatus::kPlayStatusPlay; + let cmd = if playing { self.commands.try_recv().ok() } else { Some(self.commands.recv().unwrap()) @@ -98,7 +99,11 @@ impl PlayerInternal { stream.stop().unwrap(); } state.end_of_track = false; - state.status = PlayStatus::kPlayStatusLoading; + state.status = if play { + PlayStatus::kPlayStatusPlay + } else { + PlayStatus::kPlayStatusPause + }; state.position_ms = position; state.position_measured_at = util::now_ms(); return true; diff --git a/src/session.rs b/src/session.rs index 38979601..d20ccb0b 100644 --- a/src/session.rs +++ b/src/session.rs @@ -1,35 +1,38 @@ use crypto::digest::Digest; +use crypto::mac::Mac; use crypto::sha1::Sha1; +use crypto::hmac::Hmac; use eventual::Future; use protobuf::{self, Message}; use rand::thread_rng; -use std::sync::{Mutex, RwLock, Arc, mpsc}; +use std::io::Write; use std::path::PathBuf; +use std::sync::{Mutex, RwLock, Arc, mpsc}; +use audio_key::{AudioKeyManager, AudioKey, AudioKeyError}; +use audio_file::{AudioFileManager, AudioFile}; use connection::{self, PlainConnection, CipherConnection}; -use keys::PrivateKeys; +use connection::PacketHandler; +use diffie_hellman::DHLocalKeys; use librespot_protocol as protocol; -use util::{SpotifyId, FileId, mkdir_existing}; - use mercury::{MercuryManager, MercuryRequest, MercuryResponse}; use metadata::{MetadataManager, MetadataRef, MetadataTrait}; use stream::{StreamManager, StreamEvent}; -use audio_key::{AudioKeyManager, AudioKey, AudioKeyError}; -use audio_file::{AudioFileManager, AudioFile}; -use connection::PacketHandler; +use util::{SpotifyId, FileId, mkdir_existing}; use util; pub struct Config { pub application_key: Vec, pub user_agent: String, - pub device_id: String, + pub device_name: String, pub cache_location: PathBuf, } pub struct SessionData { pub country: String, pub canonical_username: String, + pub device_id: String, } pub struct SessionInternal { @@ -41,24 +44,45 @@ pub struct SessionInternal { stream: Mutex, audio_key: Mutex, audio_file: Mutex, - rx_connection: Mutex, - tx_connection: Mutex, + rx_connection: Mutex>, + tx_connection: Mutex>, } #[derive(Clone)] pub struct Session(pub Arc); impl Session { - pub fn new(mut config: Config) -> Session { - config.device_id = { + pub fn new(config: Config) -> Session { + mkdir_existing(&config.cache_location).unwrap(); + + let device_id = { let mut h = Sha1::new(); - h.input_str(&config.device_id); + h.input_str(&config.device_name); h.result_str() }; - mkdir_existing(&config.cache_location).unwrap(); + Session(Arc::new(SessionInternal { + config: config, + data: RwLock::new(SessionData { + country: String::new(), + canonical_username: String::new(), + device_id: device_id, + }), + + rx_connection: Mutex::new(None), + tx_connection: Mutex::new(None), + + mercury: Mutex::new(MercuryManager::new()), + metadata: Mutex::new(MetadataManager::new()), + stream: Mutex::new(StreamManager::new()), + audio_key: Mutex::new(AudioKeyManager::new()), + audio_file: Mutex::new(AudioFileManager::new()), + })) + } + + pub fn connect(&self) { + let local_keys = DHLocalKeys::random(&mut thread_rng()); - let keys = PrivateKeys::new(); let mut connection = PlainConnection::connect().unwrap(); let request = protobuf_init!(protocol::keyexchange::ClientHello::new(), { @@ -82,7 +106,7 @@ impl Session { ], */ login_crypto_hello.diffie_hellman => { - gc: keys.public_key(), + gc: local_keys.public_key(), server_keys_known: 1, }, client_nonce: util::rand_vec(&mut thread_rng(), 0x10), @@ -100,75 +124,55 @@ impl Session { let response : protocol::keyexchange::APResponseMessage = protobuf::parse_from_bytes(&init_server_packet[4..]).unwrap(); - protobuf_bind!(response, { - challenge => { - login_crypto_challenge.diffie_hellman => { - gs: remote_key, - } - } - }); + let remote_key = response.get_challenge() + .get_login_crypto_challenge() + .get_diffie_hellman() + .get_gs(); - let shared_keys = keys.add_remote_key(remote_key, &init_client_packet, &init_server_packet); + 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: shared_keys.challenge().to_vec() + hmac: challenge }, pow_response => {}, crypto_response => {}, }); + connection.send_packet(&packet.write_to_bytes().unwrap()).unwrap(); - let cipher_connection = connection.setup_cipher(shared_keys); + let cipher_connection = CipherConnection::new( + connection.into_stream(), + &send_key, + &recv_key); - Session(Arc::new(SessionInternal { - config: config, - data: RwLock::new(SessionData { - country: String::new(), - canonical_username: String::new(), - }), - - rx_connection: Mutex::new(cipher_connection.clone()), - tx_connection: Mutex::new(cipher_connection), - - mercury: Mutex::new(MercuryManager::new()), - metadata: Mutex::new(MetadataManager::new()), - stream: Mutex::new(StreamManager::new()), - audio_key: Mutex::new(AudioKeyManager::new()), - audio_file: Mutex::new(AudioFileManager::new()), - })) - } - - pub fn login(&self, username: String, password: String) { - let packet = protobuf_init!(protocol::authentication::ClientResponseEncrypted::new(), { - login_credentials => { - username: username, - typ: protocol::authentication::AuthenticationType::AUTHENTICATION_USER_PASS, - auth_data: password.into_bytes(), - }, - system_info => { - cpu_family: protocol::authentication::CpuFamily::CPU_UNKNOWN, - os: protocol::authentication::Os::OS_UNKNOWN, - system_information_string: "librespot".to_owned(), - device_id: self.0.config.device_id.clone() - }, - version_string: util::version::version_string(), - appkey => { - version: self.0.config.application_key[0] as u32, - devkey: self.0.config.application_key[0x1..0x81].to_vec(), - signature: self.0.config.application_key[0x81..0x141].to_vec(), - useragent: self.0.config.user_agent.clone(), - callback_hash: vec![0; 20], - } - }); - - self.send_packet(0xab, &packet.write_to_bytes().unwrap()).unwrap(); + *self.0.rx_connection.lock().unwrap() = Some(cipher_connection.clone()); + *self.0.tx_connection.lock().unwrap() = Some(cipher_connection); } pub fn poll(&self) { - let (cmd, data) = - self.0.rx_connection.lock().unwrap().recv_packet().unwrap(); + let (cmd, data) = self.recv(); match cmd { 0x4 => self.send_packet(0x49, &data).unwrap(), @@ -179,23 +183,17 @@ impl Session { self.0.data.write().unwrap().country = String::from_utf8(data).unwrap(); }, - 0xb2...0xb6 => self.0.mercury.lock().unwrap().handle(cmd, data), - 0xac => { - let welcome_data : protocol::authentication::APWelcome = - protobuf::parse_from_bytes(&data).unwrap(); - self.0.data.write().unwrap().canonical_username = - welcome_data.get_canonical_username().to_string(); - eprintln!("Authentication succeeded") - }, - - 0xad => eprintln!("Authentication failed"), _ => () } } + pub fn recv(&self) -> (u8, Vec) { + self.0.rx_connection.lock().unwrap().as_mut().unwrap().recv_packet().unwrap() + } + pub fn send_packet(&self, cmd: u8, data: &[u8]) -> connection::Result<()> { - self.0.tx_connection.lock().unwrap().send_packet(cmd, data) + self.0.tx_connection.lock().unwrap().as_mut().unwrap().send_packet(cmd, data) } pub fn audio_key(&self, track: SpotifyId, file: FileId) -> Future { diff --git a/src/spirc.rs b/src/spirc.rs index 29c7790c..65618e72 100644 --- a/src/spirc.rs +++ b/src/spirc.rs @@ -11,9 +11,9 @@ use mercury::{MercuryRequest, MercuryMethod}; use librespot_protocol as protocol; pub use librespot_protocol::spirc::PlayStatus; -pub struct SpircManager<'s, D: SpircDelegate> { +pub struct SpircManager { delegate: D, - session: &'s Session, + session: Session, state_update_id: i64, seq_nr: u32, @@ -58,18 +58,22 @@ pub trait SpircState { fn end_of_track(&self) -> bool; } -impl <'s, D: SpircDelegate> SpircManager<'s, D> { - pub fn new(session: &'s Session, delegate: D, - name: String) -> SpircManager<'s, D> { +impl SpircManager { + pub fn new(session: Session, delegate: D) + -> SpircManager { + + let ident = session.0.data.read().unwrap().device_id.clone(); + let name = session.0.config.device_name.clone(); + SpircManager { delegate: delegate, - session: &session, + session: session, state_update_id: 0, seq_nr: 0, name: name, - ident: session.0.config.device_id.clone(), + ident: ident, device_type: 5, can_play: true, @@ -93,6 +97,8 @@ impl <'s, D: SpircDelegate> SpircManager<'s, D> { self.session.0.data.read().unwrap().canonical_username.clone())); let updates = self.delegate.updates(); + self.notify(true, None); + loop { select! { pkt = rx.recv() => { @@ -119,7 +125,7 @@ impl <'s, D: SpircDelegate> SpircManager<'s, D> { self.delegate.load(track, true, 0); } else { self.state_update_id = update_time.unwrap(); - self.notify(None); + self.notify(false, None); } } } @@ -133,7 +139,7 @@ impl <'s, D: SpircDelegate> SpircManager<'s, D> { } match frame.get_typ() { protocol::spirc::MessageType::kMessageTypeHello => { - self.notify(Some(frame.get_ident())); + self.notify(false, Some(frame.get_ident())); } protocol::spirc::MessageType::kMessageTypeLoad => { if !self.is_active { @@ -171,13 +177,18 @@ impl <'s, D: SpircDelegate> SpircManager<'s, D> { } } - fn notify(&mut self, recipient: Option<&str>) { + fn notify(&mut self, hello: bool, recipient: Option<&str>) { let mut pkt = protobuf_init!(protocol::spirc::Frame::new(), { version: 1, ident: self.ident.clone(), protocol_version: "2.0.0".to_owned(), seq_nr: { self.seq_nr += 1; self.seq_nr }, - typ: protocol::spirc::MessageType::kMessageTypeNotify, + typ: if hello { + protocol::spirc::MessageType::kMessageTypeHello + } else { + protocol::spirc::MessageType::kMessageTypeNotify + }, + device_state: self.device_state(), recipient: protobuf::RepeatedField::from_vec( recipient.map(|r| vec![r.to_owned()] ).unwrap_or(vec![])