diff --git a/Cargo.lock b/Cargo.lock index 3991775f..9b66a41e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1335,6 +1335,7 @@ dependencies = [ "base64", "block-modes", "dns-sd", + "form_urlencoded", "futures-core", "futures-util", "hmac", @@ -1363,6 +1364,7 @@ dependencies = [ "byteorder", "bytes", "env_logger", + "form_urlencoded", "futures-core", "futures-util", "hmac", @@ -1640,6 +1642,7 @@ dependencies = [ "autocfg", "num-integer", "num-traits", + "rand", ] [[package]] diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 1331b627..052cf6a5 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -11,6 +11,7 @@ edition = "2018" aes-ctr = "0.6" base64 = "0.13" block-modes = "0.7" +form_urlencoded = "1.0" futures-core = "0.3" futures-util = { version = "0.3", default_features = false } hmac = "0.10" diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 1a7f64ec..4fcb025a 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -7,7 +7,6 @@ use crate::core::config::{ConnectConfig, VolumeCtrl}; use crate::core::mercury::{MercuryError, MercurySender}; use crate::core::session::Session; use crate::core::spotify_id::{SpotifyAudioType, SpotifyId, SpotifyIdError}; -use crate::core::util::url_encode; use crate::core::util::SeqGenerator; use crate::core::version; use crate::playback::mixer::Mixer; @@ -244,6 +243,10 @@ fn volume_to_mixer(volume: u16, volume_ctrl: &VolumeCtrl) -> u16 { } } +fn url_encode(bytes: impl AsRef<[u8]>) -> String { + form_urlencoded::byte_serialize(bytes.as_ref()).collect() +} + impl Spirc { pub fn new( config: ConnectConfig, @@ -256,7 +259,7 @@ impl Spirc { let ident = session.device_id().to_owned(); // Uri updated in response to issue #288 - debug!("canonical_username: {}", url_encode(&session.username())); + debug!("canonical_username: {}", &session.username()); let uri = format!("hm://remote/user/{}/", url_encode(&session.username())); let subscription = Box::pin( diff --git a/core/Cargo.toml b/core/Cargo.toml index 4e9b03c9..29f4f332 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -17,6 +17,7 @@ aes = "0.6" base64 = "0.13" byteorder = "1.4" bytes = "1.0" +form_urlencoded = "1.0" futures-core = { version = "0.3", default-features = false } futures-util = { version = "0.3", default-features = false, features = ["alloc", "bilock", "unstable", "sink"] } hmac = "0.10" @@ -25,7 +26,7 @@ http = "0.2" hyper = { version = "0.14", optional = true, features = ["client", "tcp", "http1"] } hyper-proxy = { version = "0.9.1", optional = true, default-features = false } log = "0.4" -num-bigint = "0.4" +num-bigint = { version = "0.4", features = ["rand"] } num-integer = "0.1" num-traits = "0.2" once_cell = "1.5.2" diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index 879df67c..6f802ab5 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -1,7 +1,7 @@ use byteorder::{BigEndian, ByteOrder, WriteBytesExt}; use hmac::{Hmac, Mac, NewMac}; use protobuf::{self, Message}; -use rand::thread_rng; +use rand::{thread_rng, RngCore}; use sha1::Sha1; use std::io; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; @@ -11,7 +11,6 @@ use super::codec::ApCodec; use crate::diffie_hellman::DhLocalKeys; use crate::protocol; use crate::protocol::keyexchange::{APResponseMessage, ClientHello, ClientResponsePlaintext}; -use crate::util; pub async fn handshake( mut connection: T, @@ -40,6 +39,9 @@ async fn client_hello(connection: &mut T, gc: Vec) -> io::Result> where T: AsyncWrite + Unpin, { + let mut client_nonce = vec![0; 0x10]; + thread_rng().fill_bytes(&mut client_nonce); + let mut packet = ClientHello::new(); packet .mut_build_info() @@ -59,7 +61,7 @@ where .mut_login_crypto_hello() .mut_diffie_hellman() .set_server_keys_known(1); - packet.set_client_nonce(util::rand_vec(&mut thread_rng(), 0x10)); + packet.set_client_nonce(client_nonce); packet.set_padding(vec![0x1e]); let mut buffer = vec![0, 4]; diff --git a/core/src/diffie_hellman.rs b/core/src/diffie_hellman.rs index 42d1fd9c..57caa029 100644 --- a/core/src/diffie_hellman.rs +++ b/core/src/diffie_hellman.rs @@ -1,11 +1,11 @@ -use num_bigint::BigUint; +use num_bigint::{BigUint, RandBigInt}; +use num_integer::Integer; +use num_traits::{One, Zero}; use once_cell::sync::Lazy; -use rand::Rng; +use rand::{CryptoRng, Rng}; -use crate::util; - -pub static DH_GENERATOR: Lazy = Lazy::new(|| BigUint::from_bytes_be(&[0x02])); -pub static DH_PRIME: Lazy = Lazy::new(|| { +static DH_GENERATOR: Lazy = Lazy::new(|| BigUint::from_bytes_be(&[0x02])); +static DH_PRIME: Lazy = Lazy::new(|| { 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, @@ -17,17 +17,31 @@ pub static DH_PRIME: Lazy = Lazy::new(|| { ]) }); +fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint { + let mut base = base.clone(); + let mut exp = exp.clone(); + let mut result: BigUint = One::one(); + + while !exp.is_zero() { + if exp.is_odd() { + result = (result * &base) % modulus; + } + exp >>= 1; + base = (&base * &base) % modulus; + } + + result +} + 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); + pub fn random(rng: &mut R) -> DhLocalKeys { + let private_key = rng.gen_biguint(95 * 8); + let public_key = powm(&DH_GENERATOR, &private_key, &DH_PRIME); DhLocalKeys { private_key, @@ -40,7 +54,7 @@ impl DhLocalKeys { } pub fn shared_secret(&self, remote_key: &[u8]) -> Vec { - let shared_key = util::powm( + let shared_key = powm( &BigUint::from_bytes_be(remote_key), &self.private_key, &DH_PRIME, diff --git a/core/src/lib.rs b/core/src/lib.rs index 320967f7..bb3e21d5 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -14,12 +14,14 @@ pub mod cache; pub mod channel; pub mod config; mod connection; +#[doc(hidden)] pub mod diffie_hellman; pub mod keymaster; pub mod mercury; mod proxytunnel; pub mod session; pub mod spotify_id; +#[doc(hidden)] pub mod util; pub mod version; diff --git a/core/src/mercury/mod.rs b/core/src/mercury/mod.rs index b920e7e6..ef04e985 100644 --- a/core/src/mercury/mod.rs +++ b/core/src/mercury/mod.rs @@ -10,7 +10,6 @@ use bytes::Bytes; use tokio::sync::{mpsc, oneshot}; use crate::protocol; -use crate::util::url_encode; use crate::util::SeqGenerator; mod types; @@ -199,7 +198,7 @@ impl MercuryManager { let header: protocol::mercury::Header = protobuf::parse_from_bytes(&header_data).unwrap(); let response = MercuryResponse { - uri: url_encode(header.get_uri()), + uri: header.get_uri().to_string(), status_code: header.get_status_code(), payload: pending.parts, }; @@ -214,8 +213,21 @@ impl MercuryManager { } else if cmd == 0xb5 { self.lock(|inner| { let mut found = false; + + // TODO: This is just a workaround to make utf-8 encoded usernames work. + // A better solution would be to use an uri struct and urlencode it directly + // before sending while saving the subscription under its unencoded form. + let mut uri_split = response.uri.split('/'); + + let encoded_uri = std::iter::once(uri_split.next().unwrap().to_string()) + .chain(uri_split.map(|component| { + form_urlencoded::byte_serialize(component.as_bytes()).collect::() + })) + .collect::>() + .join("/"); + inner.subscriptions.retain(|&(ref prefix, ref sub)| { - if response.uri.starts_with(prefix) { + if encoded_uri.starts_with(prefix) { found = true; // if send fails, remove from list of subs diff --git a/core/src/util.rs b/core/src/util.rs index c55d9601..df9ea714 100644 --- a/core/src/util.rs +++ b/core/src/util.rs @@ -1,50 +1,4 @@ -use num_bigint::BigUint; -use num_integer::Integer; -use num_traits::{One, Zero}; -use rand::Rng; use std::mem; -use std::ops::{Mul, Rem, Shr}; - -pub fn rand_vec(rng: &mut G, size: usize) -> Vec { - ::std::iter::repeat(()) - .map(|()| rng.gen()) - .take(size) - .collect() -} - -pub fn url_encode(inp: &str) -> String { - let mut encoded = String::new(); - - for c in inp.as_bytes().iter() { - match *c as char { - 'A'..='Z' | 'a'..='z' | '0'..='9' | '-' | '_' | '.' | '~' | ':' | '/' => { - encoded.push(*c as char) - } - c => encoded.push_str(format!("%{:02X}", c as u32).as_str()), - }; - } - - encoded -} - -pub fn powm(base: &BigUint, exp: &BigUint, modulus: &BigUint) -> BigUint { - let mut base = base.clone(); - let mut exp = exp.clone(); - let mut result: BigUint = One::one(); - - while !exp.is_zero() { - if exp.is_odd() { - result = result.mul(&base).rem(modulus); - } - exp = exp.shr(1); - base = (&base).mul(&base).rem(modulus); - } - - result -} - -pub trait ReadSeek: ::std::io::Read + ::std::io::Seek {} -impl ReadSeek for T {} pub trait Seq { fn next(&self) -> Self;