diff --git a/Cargo.lock b/Cargo.lock index f1250b79..274a7e6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,15 +4,24 @@ version = "0.1.0" dependencies = [ "byteorder 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", + "librespot-protocol 0.1.0", "mod_path 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "num 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)", - "protobuf 0.0.9 (git+https://github.com/plietar/rust-protobuf.git)", + "protobuf 0.0.10 (git+https://github.com/stepancheg/rust-protobuf.git)", "protobuf_macros 0.1.0 (git+https://github.com/plietar/rust-protobuf-macros.git)", "rand 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", + "readall 0.1.0 (git+https://github.com/plietar/rust-readall.git)", "rust-crypto 0.2.31 (registry+https://github.com/rust-lang/crates.io-index)", "rust-gmp 0.2.0 (git+https://github.com/plietar/rust-gmp.git)", + "shannon 0.1.0 (git+https://github.com/plietar/rust-shannon.git)", + "vergen 0.0.13 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "bitflags" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "byteorder" version = "0.3.9" @@ -20,7 +29,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "gcc" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -30,9 +39,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "libc" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "librespot-protocol" +version = "0.1.0" +dependencies = [ + "mod_path 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "protobuf 0.0.10 (git+https://github.com/stepancheg/rust-protobuf.git)", +] + [[package]] name = "mod_path" version = "0.1.5" @@ -44,36 +61,41 @@ version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "rand 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-serialize 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "protobuf" -version = "0.0.9" -source = "git+https://github.com/plietar/rust-protobuf.git#f26efc36c09602109a01885449e16d15a8494cb8" +version = "0.0.10" +source = "git+https://github.com/stepancheg/rust-protobuf.git#41fde39aed305e0fb71ef6a8d92b35ee50550bde" [[package]] name = "protobuf_macros" version = "0.1.0" -source = "git+https://github.com/plietar/rust-protobuf-macros.git#3631dbaac78e955b36fdb71bb79c9b3cdc4bd4d9" +source = "git+https://github.com/plietar/rust-protobuf-macros.git#e95dbc5bdf6c13787e2385d66d9d003afcaf9f17" [[package]] name = "rand" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "libc 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "readall" +version = "0.1.0" +source = "git+https://github.com/plietar/rust-readall.git#d2bcc1de325705230e79ba444cde2f39b469f891" + [[package]] name = "rust-crypto" version = "0.2.31" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "gcc 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "gcc 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-serialize 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", + "rustc-serialize 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", "time 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -84,15 +106,40 @@ source = "git+https://github.com/plietar/rust-gmp.git#eaf298870d63712d18f8fab6bb [[package]] name = "rustc-serialize" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "shannon" +version = "0.1.0" +source = "git+https://github.com/plietar/rust-shannon.git#83a49c3397e1e546e6079cf54a0e5b2f85c6b13f" +dependencies = [ + "shannon-sys 0.1.0 (git+https://github.com/plietar/rust-shannon.git)", +] + +[[package]] +name = "shannon-sys" +version = "0.1.0" +source = "git+https://github.com/plietar/rust-shannon.git#83a49c3397e1e546e6079cf54a0e5b2f85c6b13f" +dependencies = [ + "gcc 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "time" version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "gcc 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "gcc 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "vergen" +version = "0.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "time 0.1.25 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/Cargo.toml b/Cargo.toml index 0f330b45..2cedf810 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,11 +3,9 @@ name = "librespot" version = "0.1.0" authors = ["Paul Liétar "] build = "build.rs" -links = "gmp" -#[[bin]] -#name = "librespot" -#path = "src/main.rs" +[dependencies.librespot-protocol] +path = "protocol" [dependencies] mod_path = "*" @@ -18,7 +16,7 @@ lazy_static = "0.1.*" rust-crypto = "*" [dependencies.protobuf] -git = "https://github.com/plietar/rust-protobuf.git" +git = "https://github.com/stepancheg/rust-protobuf.git" [dependencies.protobuf_macros] git = "https://github.com/plietar/rust-protobuf-macros.git" @@ -26,3 +24,11 @@ git = "https://github.com/plietar/rust-protobuf-macros.git" [dependencies.rust-gmp] git = "https://github.com/plietar/rust-gmp.git" +[dependencies.shannon] +git = "https://github.com/plietar/rust-shannon.git" + +[dependencies.readall] +git = "https://github.com/plietar/rust-readall.git" + +[build-dependencies] +vergen = "*" diff --git a/build.rs b/build.rs index 4d006b98..21c3c8e5 100644 --- a/build.rs +++ b/build.rs @@ -1,43 +1,6 @@ -use std::env; -use std::process::{Command, Stdio}; -use std::path::Path; - -#[derive(Debug)] -enum ProtobufError { - IoError(::std::io::Error), - Other -} - -impl std::convert::From<::std::io::Error> for ProtobufError { - fn from(e: ::std::io::Error) -> ProtobufError { - ProtobufError::IoError(e) - } -} - -fn compile(prefix : &Path, files : &[&Path]) -> Result<(),ProtobufError>{ - let mut c = Command::new("protoc"); - c.arg("--rust_out").arg(env::var("OUT_DIR").unwrap()) - .arg("--proto_path").arg(prefix.to_str().unwrap()); - for f in files.iter() { - c.arg(f.to_str().unwrap()); - } - - //c.stdout(Stdio::inherit()); - c.stderr(Stdio::inherit()); - - let mut p = try!(c.spawn()); - let r = try!(p.wait()); - return match r.success() { - true => Ok(()), - false => Err(ProtobufError::Other), - }; -} +extern crate vergen; fn main() { - let prefix = Path::new("protocol"); - compile(&prefix, &[ - &prefix.join("keyexchange.proto"), - &prefix.join("authentication.proto") - ]).unwrap(); + vergen::vergen(vergen::SHORT_SHA); } diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml new file mode 100644 index 00000000..1c443b94 --- /dev/null +++ b/protocol/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "librespot-protocol" +version = "0.1.0" +authors = ["Paul Liétar "] +build = "build.rs" + +[dependencies] +mod_path = "*" + +[dependencies.protobuf] +git = "https://github.com/stepancheg/rust-protobuf.git" + diff --git a/protocol/authentication.proto b/protocol/authentication.proto deleted file mode 100644 index b1c1eb56..00000000 --- a/protocol/authentication.proto +++ /dev/null @@ -1,51 +0,0 @@ -message AuthRequest { - enum LoginMethod { - PASSWORD = 0x0; - TOKEN = 0x3; - } - - message Credentials { - optional string username = 0x0a; - required LoginMethod method = 0x14; - required bytes password = 0x1e; - } - required Credentials credentials = 0x0a; - - message Data1 { - required uint32 data0 = 0x0a; - required uint32 data1 = 0x3c; - required string partner = 0x5a; // "Partner %s %s;%s" % ("lenbrook_bluesound", brand, model) - required string deviceid = 0x64; // sha1(os_device_id).hexdigest() - } - required Data1 data1 = 0x32; - - required string version = 0x46; - - message Data3 { - required uint32 data0 = 0x01; - required bytes appkey1 = 0x02; - required bytes appkey2 = 0x03; - required string data3 = 0x04; - required bytes data4 = 0x05; - } - required Data3 data3 = 0x50; -} - -message AuthSuccess { - required string username = 0x0a; - required uint32 data1 = 0x14; - required uint32 data2 = 0x19; - required uint32 data3 = 0x1e; - required bytes data4 = 0x28; - required bytes data5 = 0x32; -} - -message AuthFailure { - required uint32 code = 0x0a; - required Data1 data1 = 0x32; - - message Data1 { - required string data0 = 0x01; - } -} - diff --git a/protocol/build.rs b/protocol/build.rs new file mode 100644 index 00000000..73cc3f83 --- /dev/null +++ b/protocol/build.rs @@ -0,0 +1,45 @@ +use std::env; +use std::process::{Command, Stdio}; +use std::path::{Path,PathBuf}; + +#[derive(Debug)] +enum ProtobufError { + IoError(::std::io::Error), + Other +} + +impl std::convert::From<::std::io::Error> for ProtobufError { + fn from(e: ::std::io::Error) -> ProtobufError { + ProtobufError::IoError(e) + } +} + +fn compile(prefix : &Path, files : &[&Path]) -> Result<(),ProtobufError>{ + let mut c = Command::new("protoc"); + c.arg("--rust_out").arg(env::var("OUT_DIR").unwrap()) + .arg("--proto_path").arg(prefix.to_str().unwrap()); + + for f in files.iter() { + c.arg(f.to_str().unwrap()); + } + + //c.stdout(Stdio::inherit()); + c.stderr(Stdio::inherit()); + + let mut p = try!(c.spawn()); + let r = try!(p.wait()); + return match r.success() { + true => Ok(()), + false => Err(ProtobufError::Other), + }; +} + +fn main() { + let root = PathBuf::from(&env::var("CARGO_MANIFEST_DIR").unwrap()); + let proto = root.join("proto"); + compile(&proto, &[ + &proto.join("keyexchange.proto"), + &proto.join("authentication.proto") + ]).unwrap(); +} + diff --git a/protocol/keyexchange.proto b/protocol/keyexchange.proto deleted file mode 100644 index 44548fc6..00000000 --- a/protocol/keyexchange.proto +++ /dev/null @@ -1,61 +0,0 @@ -message Request { - message Data0 { - required uint32 data0 = 0x0a; // 05 - required uint32 data1 = 0x1e; // 02 - required uint64 data2 = 0x28; // 10800000000 - } - - message Data2 { - message Data0 { - required bytes data0 = 0x0a; // 60 - required uint32 data1 = 0x14; // 01 - } - required Data0 data0 = 0x0a; // 65 - } - - required Data0 data0 = 0x0a; // 0d - required uint32 data1 = 0x1e; // 00 - required Data2 data2 = 0x32; // 67 - required bytes random = 0x3c; // 10 - required bytes data4 = 0x46; // 01 - required bytes data5 = 0x50; // 02 - -} - -message Response { - message Data { - message Data0 { - message Data0 { - required bytes data0 = 0x0a; // 60 - required uint32 data1 = 0x14; - required bytes data2 = 0x1e; // 100 - } - required Data0 data0 = 0x0a; - } - message Data3 { - required bytes data0 = 0x0a; - } - - required Data0 data0 = 0x0a; - required bytes data1 = 0x14; - required bytes data2 = 0x1e; - required Data3 data3 = 0x28; - required bytes data4 = 0x32; - required bytes data5 = 0x3c; - } - - required Data data = 0x0a; -} - -message ChallengePacket { - message Data0 { - message Data0 { - required bytes data0 = 0x0a; - } - required Data0 data0 = 0x0a; - } - required Data0 data0 = 0x0a; - required bytes data1 = 0x14; - required bytes data2 = 0x1e; -} - diff --git a/protocol/proto/authentication.proto b/protocol/proto/authentication.proto new file mode 100644 index 00000000..dedf63b6 --- /dev/null +++ b/protocol/proto/authentication.proto @@ -0,0 +1,165 @@ +// size=30 +message ClientResponseEncrypted { + required LoginCredentials login_credentials = 0xa; // idx=0 offset=c + optional AccountCreation account_creation = 0x14; // idx=1 offset=10 + optional FingerprintResponseUnion fingerprint_response = 0x1e; // idx=2 offset=14 + optional PeerTicketUnion peer_ticket = 0x28; // idx=3 offset=18 + required SystemInfo system_info = 0x32; // idx=4 offset=1c + optional string platform_model = 0x3c; // idx=5 offset=20 + optional string version_string = 0x46; // idx=6 offset=24 + optional LibspotifyAppKey appkey = 0x50; // idx=7 offset=28 + optional ClientInfo client_info = 0x5a; // idx=8 offset=2c +} + +// size=18 +message LoginCredentials { + optional string username = 0xa; // idx=0 offset=c + required Type typ = 0x14; // idx=1 offset=10 + optional bytes auth_data = 0x1e; // idx=2 offset=14 +} + +enum Type { + AUTHENTICATION_USER_PASS = 0x0; + AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS = 0x1; + AUTHENTICATION_STORED_FACEBOOK_CREDENTIALS = 0x2; + AUTHENTICATION_SPOTIFY_TOKEN = 0x3; + AUTHENTICATION_FACEBOOK_TOKEN = 0x4; +} + +enum AccountCreation { + ACCOUNT_CREATION_ALWAYS_PROMPT = 0x1; + ACCOUNT_CREATION_ALWAYS_CREATE = 0x3; +} + +// size=14 +message FingerprintResponseUnion { + optional FingerprintGrainResponse grain = 0xa; // idx=0 offset=c + optional FingerprintHmacRipemdResponse hmac_ripemd = 0x14; // idx=1 offset=10 +} + +// size=1c +message FingerprintGrainResponse { + required bytes encrypted_key = 0xa; // idx=0 offset=c size=f +} + +// size=20 +message FingerprintHmacRipemdResponse { + required bytes hmac = 0xa; // idx=0 offset=c size=13 +} + +// size=14 +message PeerTicketUnion { + optional PeerTicketPublicKey public_key = 0xa; // idx=0 offset=c + optional PeerTicketOld old_ticket = 0x14; // idx=1 offset=10 +} + +// size=8c +message PeerTicketPublicKey { + required bytes public_key = 0xa; // idx=0 offset=c size=7f +} + +// size=90 +message PeerTicketOld { + required bytes peer_ticket = 0xa; // idx=0 offset=c + required bytes peer_ticket_signature = 0x14; // idx=1 offset=10 size=7f +} + +// size=34 +message SystemInfo { + required CpuFamily cpu_family = 0xa; // idx=0 offset=c + optional uint32 cpu_subtype = 0x14; // idx=1 offset=10 + optional uint32 cpu_ext = 0x1e; // idx=2 offset=14 + optional Brand brand = 0x28; // idx=3 offset=18 + optional uint32 brand_flags = 0x32; // idx=4 offset=1c + required Os os = 0x3c; // idx=5 offset=20 + optional uint32 os_version = 0x46; // idx=6 offset=24 + optional uint32 os_ext = 0x50; // idx=7 offset=28 + optional string system_information_string = 0x5a; // idx=8 offset=2c + optional string device_id = 0x64; // idx=9 offset=30 +} + +enum CpuFamily { + CPU_UNKNOWN = 0x0; + CPU_X86 = 0x1; + CPU_X86_64 = 0x2; + CPU_PPC = 0x3; + CPU_PPC_64 = 0x4; + CPU_ARM = 0x5; + CPU_IA64 = 0x6; + CPU_SH = 0x7; + CPU_MIPS = 0x8; + CPU_BLACKFIN = 0x9; +} + +enum Brand { + BRAND_UNBRANDED = 0x0; + BRAND_INQ = 0x1; + BRAND_HTC = 0x2; + BRAND_NOKIA = 0x3; +} + +enum Os { + OS_UNKNOWN = 0x0; + OS_WINDOWS = 0x1; + OS_OSX = 0x2; + OS_IPHONE = 0x3; + OS_S60 = 0x4; + OS_LINUX = 0x5; + OS_WINDOWS_CE = 0x6; + OS_ANDROID = 0x7; + OS_PALM = 0x8; + OS_FREEBSD = 0x9; + OS_BLACKBERRY = 0xa; + OS_SONOS = 0xb; + OS_LOGITECH = 0xc; + OS_WP7 = 0xd; + OS_ONKYO = 0xe; + OS_PHILIPS = 0xf; + OS_WD = 0x10; + OS_VOLVO = 0x11; + OS_TIVO = 0x12; + OS_AWOX = 0x13; + OS_MEEGO = 0x14; + OS_QNXNTO = 0x15; + OS_BCO = 0x16; +} + +// size=168 +message LibspotifyAppKey { + required uint32 version = 0x1; // idx=0 offset=c + required bytes devkey = 0x2; // idx=1 offset=10 size=7f + required bytes signature = 0x3; // idx=2 offset=90 size=bf + required string useragent = 0x4; // idx=3 offset=150 + required bytes callback_hash = 0x5; // idx=4 offset=154 size=13 +} + +// size=18 +message ClientInfo { + optional bool limited = 0x1; // idx=0 offset=c + optional ClientInfoFacebook fb = 0x2; // idx=1 offset=10 + optional string language = 0x3; // idx=2 offset=14 +} + +// size=10 +message ClientInfoFacebook { + optional string machine_id = 0x1; // idx=0 offset=c +} + +message AuthSuccess { + required string username = 0x0a; + required uint32 data1 = 0x14; + required uint32 data2 = 0x19; + required uint32 data3 = 0x1e; + required bytes data4 = 0x28; + required bytes data5 = 0x32; +} + +message AuthFailure { + required uint32 code = 0x0a; + required Data1 data1 = 0x32; + + message Data1 { + required string data0 = 0x01; + } +} + diff --git a/protocol/proto/keyexchange.proto b/protocol/proto/keyexchange.proto new file mode 100644 index 00000000..464828df --- /dev/null +++ b/protocol/proto/keyexchange.proto @@ -0,0 +1,239 @@ +// size=80 +message ClientHello { + required BuildInfo build_info = 0xa; // idx=0 offset=c + repeated Fingerprint fingerprints_supported = 0x14; // idx=ffff offset=10 + repeated Cryptosuite cryptosuites_supported = 0x1e; // idx=ffff offset=2c + repeated Powscheme powschemes_supported = 0x28; // idx=ffff offset=48 + required LoginCryptoHelloUnion login_crypto_hello = 0x32; // idx=1 offset=64 + required bytes client_nonce = 0x3c; // idx=2 offset=68 size=f + optional bytes padding = 0x46; // idx=3 offset=78 + optional FeatureSet feature_set = 0x50; // idx=4 offset=7c +} + +// size=38 +message BuildInfo { + required Product product = 0xa; // idx=0 offset=c + repeated ProductFlags product_flags = 0x14; // idx=ffff offset=10 + required Platform platform = 0x1e; // idx=1 offset=2c + required uint64 version = 0x28; // idx=2 offset=30 extra=246558 +} + +enum Product { + PRODUCT_CLIENT = 0x0; + PRODUCT_LIBSPOTIFY= 0x1; + PRODUCT_MOBILE = 0x2; + PRODUCT_PARTNER = 0x3; + PRODUCT_LIBSPOTIFY_EMBEDDED = 0x5; +} + +enum ProductFlags { + PRODUCT_FLAG_NONE = 0x0; + PRODUCT_FLAG_DEV_BUILD = 0x1; +} + +enum Platform { + PLATFORM_WIN32_X86 = 0x0; + PLATFORM_OSX_X86 = 0x1; + PLATFORM_LINUX_X86 = 0x2; + PLATFORM_IPHONE_ARM = 0x3; + PLATFORM_S60_ARM = 0x4; + PLATFORM_OSX_PPC = 0x5; + PLATFORM_ANDROID_ARM = 0x6; + PLATFORM_WINDOWS_CE_ARM = 0x7; + PLATFORM_LINUX_X86_64 = 0x8; + PLATFORM_OSX_X86_64 = 0x9; + PLATFORM_PALM_ARM = 0xa; + PLATFORM_LINUX_SH = 0xb; + PLATFORM_FREEBSD_X86 = 0xc; + PLATFORM_FREEBSD_X86_64 = 0xd; + PLATFORM_BLACKBERRY_ARM = 0xe; + PLATFORM_SONOS = 0xf; + PLATFORM_LINUX_MIPS = 0x10; + PLATFORM_LINUX_ARM = 0x11; + PLATFORM_LOGITECH_ARM = 0x12; + PLATFORM_LINUX_BLACKFIN = 0x13; + PLATFORM_WP7_ARM = 0x14; + PLATFORM_ONKYO_ARM = 0x15; + PLATFORM_QNXNTO_ARM = 0x16; + PLATFORM_BCO_ARM = 0x17; +} + +enum Fingerprint { + FINGERPRINT_GRAIN = 0x0; + FINGERPRINT_HMAC_RIPEMD = 0x1; +} + +enum Cryptosuite { + CRYPTO_SUITE_SHANNON = 0x0; + CRYPTO_SUITE_RC4_SHA1_HMAC = 0x1; +} + +enum Powscheme { + POW_HASH_CASH = 0x0; +} + +// size=10 +message LoginCryptoHelloUnion { + optional LoginCryptoDiffieHellmanHello diffie_hellman = 0xa; // idx=0 offset=c +} + +// size=70 +message LoginCryptoDiffieHellmanHello { + required bytes gc = 0xa; // idx=0 offset=c size=5f + required uint32 server_keys_known = 0x14; // idx=1 offset=6c +} + +// size=10 +message FeatureSet { + optional bool autoupdate2 = 0x1; // idx=0 offset=c + optional bool current_location = 0x2; // idx=1 offset=d +} + + +// size=18 +message APResponseMessage { + optional APChallenge challenge = 0xa; // idx=0 offset=c + optional UpgradeRequiredMessage upgrade = 0x14; // idx=1 offset=10 + optional APLoginFailed login_failed = 0x1e; // idx=2 offset=14 +} + +// size=30 +message APChallenge { + required LoginCryptoChallengeUnion login_crypto_challenge = 0xa; // idx=0 offset=c + required FingerprintChallengeUnion fingerprint_challenge = 0x14; // idx=1 offset=10 + required PoWChallengeUnion pow_challenge = 0x1e; // idx=2 offset=14 + required CryptoChallengeUnion crypto_challenge = 0x28; // idx=3 offset=18 + required bytes server_nonce = 0x32; // idx=4 offset=1c size=f + optional bytes padding = 0x3c; // idx=5 offset=2c +} + +// size=10 +message LoginCryptoChallengeUnion { + optional LoginCryptoDiffieHellmanChallenge diffie_hellman = 0xa; // idx=0 offset=c +} + +// size=170 +message LoginCryptoDiffieHellmanChallenge { + required bytes gs = 0xa; // idx=0 offset=c size=5f + required int32 server_signature_key = 0x14; // idx=1 offset=6c type=int8 + required bytes gs_signature = 0x1e; // idx=2 offset=6d size=ff +} + +// size=14 +message FingerprintChallengeUnion { + optional FingerprintGrainChallenge grain = 0xa; // idx=0 offset=c + optional FingerprintHmacRipemdChallenge hmac_ripemd = 0x14; // idx=1 offset=10 +} + +// size=1c +message FingerprintGrainChallenge { + required bytes kek = 0xa; // idx=0 offset=c size=f +} + +// size=20 +message FingerprintHmacRipemdChallenge { + required bytes challenge = 0xa; // idx=0 offset=c size=13 +} + +// size=10 +message PoWChallengeUnion { + optional PoWHashCashChallenge hash_cash = 0xa; // idx=0 offset=c +} + +// size=24 +message PoWHashCashChallenge { + optional bytes prefix = 0xa; // idx=0 offset=c size=f + optional int32 length = 0x14; // idx=1 offset=1c type=int8 + optional int32 target = 0x1e; // idx=2 offset=20 +} + +// size=14 +message CryptoChallengeUnion { + optional CryptoShannonChallenge shannon = 0xa; // idx=0 offset=c + optional CryptoRc4Sha1HmacChallenge rc4_sha1_hmac = 0x14; // idx=1 offset=10 +} + +// size=8 +message CryptoShannonChallenge { +} + +// size=8 +message CryptoRc4Sha1HmacChallenge { +} + +// size=18 +message UpgradeRequiredMessage { + required bytes upgrade_signed_part = 0xa; // idx=0 offset=c + required bytes signature = 0x14; // idx=1 offset=10 + optional string http_suffix = 0x1e; // idx=2 offset=14 +} + +// size=1c +message APLoginFailed { + required ErrorCode error_code = 0xa; // idx=0 offset=c + optional int32 retry_delay = 0x14; // idx=1 offset=10 + optional int32 expiry = 0x1e; // idx=2 offset=14 + optional string error_description = 0x28; // idx=3 offset=18 +} + +enum ErrorCode { + ProtocolError = 0x0; + TryAnotherAP = 0x2; + BadConnectionId = 0x5; + TravelRestriction = 0x9; + PremiumAccountRequired = 0xb; + BadCredentials = 0xc; + CouldNotValidateCredentials = 0xd; + AccountExists = 0xe; + ExtraVerificationRequired = 0xf; + InvalidAppKey = 0x10; + ApplicationBanned = 0x11; +} + + + +// size=18 +message ClientResponsePlaintext { + required LoginCryptoResponseUnion login_crypto_response = 0xa; // idx=0 offset=c + required PoWResponseUnion pow_response = 0x14; // idx=1 offset=10 + required CryptoResponseUnion crypto_response = 0x1e; // idx=2 offset=14 +} + +// size=10 +message LoginCryptoResponseUnion { + optional LoginCryptoDiffieHellmanResponse diffie_hellman = 0xa; // idx=0 offset=c +} + +// size=20 +message LoginCryptoDiffieHellmanResponse { + required bytes hmac = 0xa; // idx=0 offset=c size=13 +} + +// size=10 +message PoWResponseUnion { + optional PoWHashCashResponse hash_cash = 0xa; // idx=0 offset=c +} + +// size=1c +message PoWHashCashResponse { + required bytes hash_suffix = 0xa; // idx=0 offset=c size=f +} + +// size=14 +message CryptoResponseUnion { + optional CryptoShannonResponse shannon = 0xa; // idx=0 offset=c + optional CryptoRc4Sha1HmacResponse rc4_sha1_hmac = 0x14; // idx=1 offset=10 +} + +// size=10 +message CryptoShannonResponse { + optional int32 dummy = 0x1; // idx=0 offset=c type=uint8 +} + +// size=10 +message CryptoRc4Sha1HmacResponse { + optional int32 dummy = 0x1; // idx=0 offset=c type=uint8 +} + + + diff --git a/protocol/mercury.proto b/protocol/proto/mercury.proto similarity index 100% rename from protocol/mercury.proto rename to protocol/proto/mercury.proto diff --git a/protocol/metadata.proto b/protocol/proto/metadata.proto similarity index 100% rename from protocol/metadata.proto rename to protocol/proto/metadata.proto diff --git a/protocol/spirc.proto b/protocol/proto/spirc.proto similarity index 100% rename from protocol/spirc.proto rename to protocol/proto/spirc.proto diff --git a/protocol/spotify.proto b/protocol/proto/spotify.proto similarity index 100% rename from protocol/spotify.proto rename to protocol/proto/spotify.proto diff --git a/src/protocol.rs b/protocol/src/lib.rs similarity index 68% rename from src/protocol.rs rename to protocol/src/lib.rs index 5f33fcbd..7bfdc7f7 100644 --- a/src/protocol.rs +++ b/protocol/src/lib.rs @@ -1,3 +1,8 @@ +#![feature(plugin)] +#![plugin(mod_path)] + +extern crate protobuf; + mod_path! keyexchange (concat!(env!("OUT_DIR"), "/keyexchange.rs")); mod_path! authentication (concat!(env!("OUT_DIR"), "/authentication.rs")); diff --git a/src/connection.rs b/src/connection.rs index 263f036f..06686cfd 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -1,43 +1,110 @@ use util; -use byteorder::{ReadBytesExt, WriteBytesExt, BigEndian}; -use std::io::{Write,Read}; +use byteorder::{self, ReadBytesExt, WriteBytesExt, BigEndian, ByteOrder}; +use keys::SharedKeys; +use readall::ReadAllExt; +use shannon::ShannonStream; +use std::convert; +use std::io; +use std::io::Write; use std::net::TcpStream; +use std::result; -pub struct Connection { - stream: TcpStream, +#[derive(Debug)] +pub enum Error { + IoError(io::Error), + Other } -impl Connection { - pub fn connect() -> Connection { - Connection { - stream: TcpStream::connect("lon3-accesspoint-a26.ap.spotify.com:4070").unwrap(), +pub type Result = result::Result; + +impl convert::From for Error { + fn from(err: io::Error) -> Error { + Error::IoError(err) + } +} + +impl convert::From for Error { + fn from(err: byteorder::Error) -> Error { + match err { + byteorder::Error::Io(e) => Error::IoError(e), + _ => Error::Other } } +} - pub fn send_packet(&mut self, data: &[u8]) -> Vec { +pub struct PlainConnection { + stream: TcpStream +} + +pub struct CipherConnection { + stream: ShannonStream, +} + +impl PlainConnection { + pub fn connect() -> Result { + Ok(PlainConnection { + stream: try!(TcpStream::connect("lon3-accesspoint-a26.ap.spotify.com:4070")), + }) + } + + pub fn send_packet(&mut self, data: &[u8]) -> Result> { self.send_packet_prefix(&[], data) } - pub fn send_packet_prefix(&mut self, prefix: &[u8], data: &[u8]) -> Vec { + pub fn send_packet_prefix(&mut self, prefix: &[u8], data: &[u8]) -> Result> { let size = prefix.len() + 4 + data.len(); let mut buf = Vec::with_capacity(size); - buf.write(prefix).unwrap(); - buf.write_u32::(size as u32).unwrap(); - buf.write(data).unwrap(); - self.stream.write(&buf).unwrap(); + try!(buf.write(prefix)); + try!(buf.write_u32::(size as u32)); + try!(buf.write(data)); + try!(self.stream.write(&buf)); + try!(self.stream.flush()); - buf + Ok(buf) } - pub fn recv_packet(&mut self) -> Vec { - let size : usize = self.stream.read_u32::().unwrap() as usize; - let mut buffer = util::alloc_buffer(size - 4); + pub fn recv_packet(&mut self) -> Result> { + let size = try!(self.stream.read_u32::()) as usize; + let mut buffer = util::alloc_buffer(size); - self.stream.read(&mut buffer).unwrap(); + BigEndian::write_u32(&mut buffer, size as u32); + try!(self.stream.read_all(&mut buffer[4..])); - buffer + Ok(buffer) + } + + pub fn setup_cipher(self, keys: SharedKeys) -> CipherConnection { + CipherConnection{ + stream: ShannonStream::new(self.stream, &keys.send_key(), &keys.recv_key()) + } } } +impl CipherConnection { + pub fn send_encrypted_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)); + + try!(self.stream.finish_send()); + try!(self.stream.flush()); + + Ok(()) + } + + pub fn recv_packet(&mut self) -> Result<(u8, Vec)> { + let cmd = try!(self.stream.read_u8()); + let size = try!(self.stream.read_u16::()) as usize; + + let mut data = vec![0; size]; + try!(self.stream.read_all(&mut data)); + + try!(self.stream.finish_recv()); + + Ok((cmd, data)) + } +} + + + diff --git a/src/cryptoutil.rs b/src/keys.rs similarity index 54% rename from src/cryptoutil.rs rename to src/keys.rs index 46cb31fb..3fb0a13d 100644 --- a/src/cryptoutil.rs +++ b/src/keys.rs @@ -1,6 +1,6 @@ use rand; use gmp::Mpz; -use std::num::FromPrimitive; +use num::FromPrimitive; use crypto; use crypto::mac::Mac; use std::io::Write; @@ -23,69 +23,80 @@ lazy_static! { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff ]); } -pub struct Crypto { +pub struct PrivateKeys { private_key: Mpz, public_key: Mpz, - shared: Option, } pub struct SharedKeys { - pub challenge: Vec, - pub send_key: Vec, - pub recv_key: Vec + private: PrivateKeys, + challenge: Vec, + send_key: Vec, + recv_key: Vec } -impl Crypto { - pub fn new() -> Crypto { +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]) -> Crypto { + pub fn new_with_key(key_data: &[u8]) -> PrivateKeys { let private_key = Mpz::from_bytes_be(key_data); let public_key = DH_GENERATOR.powm(&private_key, &DH_PRIME); - Crypto { + PrivateKeys { private_key: private_key, public_key: public_key, - shared: None, } } - pub fn setup(&mut self, remote_key: &[u8], client_packet: &[u8], server_packet: &[u8]) { - let shared_key = Mpz::from_bytes_be(remote_key).powm(&self.private_key, &DH_PRIME); - - let mut data = Vec::with_capacity(0x54); - let mut h = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &shared_key.to_bytes_be()); - - for i in 1..6 { - h.input(client_packet); - h.input(server_packet); - h.input(&[i]); - data.write(&h.result().code()).unwrap(); - h.reset(); - } - - h = crypto::hmac::Hmac::new(crypto::sha1::Sha1::new(), &data[..0x14]); - h.input(client_packet); - h.input(server_packet); - - self.shared = Some(SharedKeys{ - challenge: h.result().code().to_vec(), - send_key: data[0x14..0x34].to_vec(), - recv_key: data[0x34..0x54].to_vec() - }); + 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 shared(&self) -> &SharedKeys { - match self.shared { - Some(ref shared) => shared, - None => panic!("ABC") + pub fn add_remote_key(self, remote_key: &[u8], client_packet: &[u8], server_packet: &[u8]) -> SharedKeys { + let shared_key = Mpz::from_bytes_be(remote_key).powm(&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/main.rs b/src/main.rs index fd0f7437..97b5ae76 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,7 @@ #![crate_name = "librespot"] -#![feature(plugin,core)] +#![feature(plugin)] -#![plugin(mod_path)] #![plugin(protobuf_macros)] #[macro_use] extern crate lazy_static; @@ -11,18 +10,39 @@ extern crate crypto; extern crate gmp; extern crate num; extern crate protobuf; +extern crate shannon; extern crate rand; +extern crate readall; + +extern crate librespot_protocol; mod connection; -mod cryptoutil; -mod protocol; +mod keys; mod session; mod util; -use session::Session; +use std::fs::File; +use std::io::Read; +use std::path::Path; + +use session::{Session,Config}; fn main() { - let mut s = Session::new(); - s.login(); + let mut args = std::env::args().skip(1); + let mut appkey_file = File::open(Path::new(&args.next().unwrap())).unwrap(); + let username = args.next().unwrap(); + let password = args.next().unwrap(); + + let mut appkey = Vec::new(); + appkey_file.read_to_end(&mut appkey).unwrap(); + + let config = Config { + application_key: appkey, + user_agent: "ABC".to_string(), + device_id: "ABC".to_string() + }; + let mut s = Session::new(config); + + s.login(username, password); } diff --git a/src/session.rs b/src/session.rs index b5912ff8..28bd5746 100644 --- a/src/session.rs +++ b/src/session.rs @@ -1,78 +1,132 @@ -use connection::Connection; -use cryptoutil::Crypto; -use protocol; +use connection::{PlainConnection, CipherConnection}; +use keys::PrivateKeys; +use librespot_protocol as protocol; use util; -use std::iter::{FromIterator,repeat}; +use crypto::sha1::Sha1; +use crypto::digest::Digest; use protobuf::*; use rand::thread_rng; +pub struct Config { + pub application_key: Vec, + pub user_agent: String, + pub device_id: String, +} + pub struct Session { - connection: Connection, - crypto: Crypto, + config: Config, + connection: CipherConnection, } impl Session { - pub fn new() -> Session { - Session { - connection: Connection::connect(), - crypto: Crypto::new(), - } - } + pub fn new(mut config: Config) -> Session { + config.device_id = { + let mut h = Sha1::new(); + h.input_str(&config.device_id); + h.result_str() + }; - pub fn login(&mut self) { - let request = protobuf_init!(protocol::keyexchange::Request::new(), { - data0 => { - data0: 0x05, - data1: 0x01, - data2: 0x10800000000, + + let keys = PrivateKeys::new(); + let mut connection = PlainConnection::connect().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, }, - data1: 0, - data2.data0 => { - data0: self.crypto.public_key(), - data1: 1, + /* + 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: keys.public_key(), + server_keys_known: 1, }, - random: util::rand_vec(&mut thread_rng(), 0x10), - data4: vec![0x1e], - data5: vec![0x08, 0x01] + client_nonce: util::rand_vec(&mut thread_rng(), 0x10), + padding: vec![0x1e], + feature_set => { + autoupdate2: true, + } }); let init_client_packet = - self.connection.send_packet_prefix(&[0,4], &request.write_to_bytes().unwrap()); + connection.send_packet_prefix(&[0,4], &request.write_to_bytes().unwrap()).unwrap(); let init_server_packet = - self.connection.recv_packet(); + connection.recv_packet().unwrap(); - let response : protocol::keyexchange::Response = - parse_from_bytes(&init_server_packet).unwrap(); + let response : protocol::keyexchange::APResponseMessage = + parse_from_bytes(&init_server_packet[4..]).unwrap(); - protobuf_bind!(response, { data.data0.data0.data0: remote_key }); - - self.crypto.setup(&remote_key, &init_client_packet, &init_server_packet); - - return; - let appkey = vec![]; - let request = protobuf_init!(protocol::authentication::AuthRequest::new(), { - credentials => { - username: "USERNAME".to_string(), - method: protocol::authentication::AuthRequest_LoginMethod::PASSWORD, - password: b"PASSWORD".to_vec(), - }, - data1 => { - data0: 0, - data1: 0, - partner: "Partner blabla".to_string(), - deviceid: "abc".to_string() - }, - version: "master-v1.8.0-gad9e5b46".to_string(), - data3 => { - data0: 1, - appkey1: appkey[0x1..0x81].to_vec(), - appkey2: appkey[0x81..0x141].to_vec(), - data3: "".to_string(), - data4: Vec::from_iter(repeat(0).take(20)) + protobuf_bind!(response, { + challenge => { + login_crypto_challenge.diffie_hellman => { + gs: remote_key, + } } }); - //println!("{:?}", response); + + let shared_keys = keys.add_remote_key(remote_key, &init_client_packet, &init_server_packet); + + let packet = protobuf_init!(protocol::keyexchange::ClientResponsePlaintext::new(), { + login_crypto_response.diffie_hellman => { + hmac: shared_keys.challenge().to_vec() + }, + pow_response => {}, + crypto_response => {}, + }); + + connection.send_packet(&packet.write_to_bytes().unwrap()).unwrap(); + + Session { + config: config, + connection: connection.setup_cipher(shared_keys) + } + } + + pub fn login(&mut self, username: String, password: String) { + let packet = protobuf_init!(protocol::authentication::ClientResponseEncrypted::new(), { + login_credentials => { + username: username, + typ: protocol::authentication::Type::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_string(), + device_id: self.config.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], + } + }); + + self.connection.send_encrypted_packet( + 0xab, + &packet.write_to_bytes().unwrap()).unwrap(); + + loop { + let (cmd, data) = self.connection.recv_packet().unwrap(); + println!("{:x}", cmd); + } } } diff --git a/src/util.rs b/src/util.rs index 5f892dff..379b77b7 100644 --- a/src/util.rs +++ b/src/util.rs @@ -18,3 +18,12 @@ pub fn alloc_buffer(size: usize) -> Vec { vec } + +pub mod version { + include!(concat!(env!("OUT_DIR"), "/version.rs")); + + pub fn version_string() -> String { + format!("librespot-{}", short_sha()) + } +} +