Reversing Spotify Connect protocol

Spotify has been promising for months now to release "Spotify Connect" as a part of libspotify. Instead, they are steadily striking "exclusive deals" with one manufacturer after another (called "Spotify Partners") but knowingly ignore "Spotify Customers" like me, with slightly older hardware, but who might have been more loyal to Spotify then any new hardware buyers, loyal customers who are now desperately locked waiting for improbable firmware updates..

Onkyo also promised to add Spotify Connect to it's receivers line (specifically they mentioned NR-636 model). I have a sour feeling that my NR-626 model, bought just in October 2013, is not going to be supported (even though it is fully capable to).

Such marketing sucks ass. I already decided that I am not going to choose Onkyo for my next amp upgrade. And I might switch away from Spotify after paying them for 3 years too.

So yeah, fuck them - I'll do this stuff myself.

I was able to extract Spotify Connect implementation from one speakers manufacturer firmware, finally! (won't mention names for obvious reasons ;-).

So I am reverse-engineering Spotify Connect at the moment and it doesn't seem too hard. It is a relatively-simple protocol. Much simplier than what libspotify does.

Spotify Connect Protocol

The ARM library is rather small (161 kb) and is called libspotify_embedded.so.

It's relevant exports:

 4: 00014c1c   152 FUNC    GLOBAL DEFAULT   10 SpPlaybackSetBitrate
 5: 00014f14   164 FUNC    GLOBAL DEFAULT   10 SpConnectionLoginPassword
 6: 00015354   152 FUNC    GLOBAL DEFAULT   10 SpPlaybackIsPlaying
19: 00014cb4   152 FUNC    GLOBAL DEFAULT   10 SpConnectionIsLoggedIn
21: 0001505c   152 FUNC    GLOBAL DEFAULT   10 SpPlaybackEnableRepeat
23: 00014de4   152 FUNC    GLOBAL DEFAULT   10 SpConnectionSetConnectivity
24: 00014ac0   184 FUNC    GLOBAL DEFAULT   10 SpGetMetadataImageURL
25: 0001577c   152 FUNC    GLOBAL DEFAULT   10 SpPlaybackPause
27: 000159f4   164 FUNC    GLOBAL DEFAULT   10 SpRegisterConnectionCallbacks
28: 00015bc8     4 FUNC    GLOBAL DEFAULT   10 SpInit
33: 000146bc   164 FUNC    GLOBAL DEFAULT   10 SpPlayPreset
37: 000155b4   152 FUNC    GLOBAL DEFAULT   10 SpPlaybackSeek
39: 00014620   156 FUNC    GLOBAL DEFAULT   10 SpGetPreset
41: 00014fb8   164 FUNC    GLOBAL DEFAULT   10 SpConnectionLoginBlob
42: 00015224   152 FUNC    GLOBAL DEFAULT   10 SpPlaybackIsRepeated
43: 000153ec   152 FUNC    GLOBAL DEFAULT   10 SpPlaybackGetVolume
45: 00015a98   152 FUNC    GLOBAL DEFAULT   10 SpFree
46: 00015950   164 FUNC    GLOBAL DEFAULT   10 SpRegisterPlaybackCallbacks
47: 00015814   152 FUNC    GLOBAL DEFAULT   10 SpPlaybackPlay
53: 0001551c   152 FUNC    GLOBAL DEFAULT   10 SpPlaybackGetPosition
54: 0001564c   152 FUNC    GLOBAL DEFAULT   10 SpPlaybackSkipToPrev
55: 000156e4   152 FUNC    GLOBAL DEFAULT   10 SpPlaybackSkipToNext
57: 00015b30   152 FUNC    GLOBAL DEFAULT   10 SpGetLibraryVersion
64: 00014a14   172 FUNC    GLOBAL DEFAULT   10 SpGetMetadataValidRange
68: 0001497c   152 FUNC    GLOBAL DEFAULT   10 SpSetDisplayName
69: 00014d4c   152 FUNC    GLOBAL DEFAULT   10 SpConnectionLogout
70: 00014760   176 FUNC    GLOBAL DEFAULT   10 SpConnectionLoginZeroConf
72: 00014e7c   152 FUNC    GLOBAL DEFAULT   10 SpConnectionLoginOauthToken
74: 000152bc   152 FUNC    GLOBAL DEFAULT   10 SpPlaybackIsShuffled
78: 00015484   152 FUNC    GLOBAL DEFAULT   10 SpPlaybackUpdateVolume
81: 000158ac   164 FUNC    GLOBAL DEFAULT   10 SpRegisterDebugCallbacks
82: 00014b78   164 FUNC    GLOBAL DEFAULT   10 SpGetMetadata
83: 000148a8   212 FUNC    GLOBAL DEFAULT   10 SpPumpEvents
90: 0001518c   152 FUNC    GLOBAL DEFAULT   10 SpPlaybackIsActiveDevice
94: 00014810   152 FUNC    GLOBAL DEFAULT   10 SpZeroConfGetVars
97: 000150f4   152 FUNC    GLOBAL DEFAULT   10 SpPlaybackEnableShuffle

That's pretty much it ;)

How it works in a nutshell

It advertises itself via Zeroconf (avahi-publish) as a local _spotify-connect._tcp service, which runs a simple HTTP service.

When iOS client is started, it tries to resolve this service and connects to it if available.

Client then served with a following HTTP response:

    {"status": 101, "statusString": "OK", "spotifyError": 0, "version": "2.0.0", "deviceID": "00:00:00:00:00:00", "publicKey": "gjigiijiwjiwjrijkdaldklasdaldladkdkladlasdksldkladklalkdlakdlaskdlaskldkadkldklaskdlskdlaskdlakdldkasldkasldklsakdlaskdakdkadasd", "remoteName": "MY SPEAKERS", "activeUser": ""}

At this moment, "MY SPEAKERS" appear under "Spotify connect" menu as a device on the network.

After it's selected, client sends "addUser" command, where it passes userName, blob and a clientKey - everything needed for the sign-in to Spotify.

So the library then establish connection with one of Spotify Access Points via SpConnectionLoginBlob(username, blob).

"Spotify connect" icon goes green.

Now all communication happens through Spotify server in a sort-of IRC like protocol called Gaia(?) v2.0.

These are the commands it supports:

"load",0
"play",0
"pause",0
"seek",0
"skip-prev",0 "skip-next",0 "volume",0
"shuffle",0
"repeat",0
"replace",0
"fallback",0
"pull",0
"error",0
"goodbye",0
"logout",0

You see, it's pretty basic ;) Much simplier than doing it all via libspotify.

I haven't found any heavy encryption AES stuff in the library (yet?) and only some simplier encryption routines, so hopefully it just uses public key encryption on the stream.

Running it with player binaries under Qemu-arm, I was able to make my PC receive PCM sound stream, whilst controlling songs with iPhone Spotify client. Neat :)

But that's just half-work (or rather 15%). The library must be written from scratch to be usable and to support all platforms. Further analysing all the logic and converting it to a high-level language is the hardest part.

As much as I'd like to make and release this project as open-source, I straggle to find time at the moment to commit daily on this, so I am calling for collaborators. Drop me a line if you want to achieve this honourable task together!;)