diff --git a/README.md b/README.md index a3c5fb7..96786b3 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,38 @@ # SpotifyKeyDumper ### By [@ProfessorTox](https://twitter.com/ProfessorTox) + +[![Discord](https://img.shields.io/discord/782069040713039882.svg?color=7389D8&label=SpotifyKeyDumper%20&logo=discord&logoColor=FFFFFF)](https://discord.gg/yPFQ9epABz) + Dump AES keys for Spotify songs from a compatible Windows Spotify version (compatibility listed below). -Now with automatic download support (compatible versions listed below)! - -* Note: *Currently not compatible with premium accounts* - -![Screenshot Example](./screenshot_example.png) - -## Compatibility -Crossed out items support key dumping but not automatic downloading -* ~~1.1.25~~ -* ~~1.1.26~~ -* ~~1.1.27~~ -* ~~1.1.28~~ -* ~~1.1.29~~ -* ~~1.1.30~~ -* ~~1.1.44~~ -* 1.1.45 -* 1.1.46 -* 1.1.47 +**Now with automatic download support for songs and podcast episodes!** ## Using -1. Make sure `SpotifyKeyDumperInjector.exe` and `SpotifyKeyDumper.dll` are located in the same folder as Spotify (`Spotify.exe`). -2. Start SpotifyKeyDumperInjector before launching Spotify. +1. Go to `%appdata%\Spotify` (or wherever your Spotify installation is located) +2. Make sure `SpotifyKeyDumperInjector.exe` and `SpotifyKeyDumper.dll` are located in the same place as `Spotify.exe`. +3. Start SpotifyKeyDumperInjector (requires administrator) before launching Spotify. +4. Each song or podcast episode (after the first one) that plays will be automatically downloaded and placed + under `%appdata%\Spotify\Downloads`. + +## Compatibility +* *Crossed out items support key dumping but not automatic downloading.* + +Spotify version: +* 1.1.47 +* 1.1.46 +* 1.1.45 +* ~~1.1.44~~ +* ~~1.1.30~~ +* ~~1.1.29~~ +* ~~1.1.28~~ +* ~~1.1.27~~ +* ~~1.1.26~~ +* ~~1.1.25~~ ## Building -This project uses C++14 on Visual Studio 2019 +This project uses C++14 on Visual Studio 2019. -If you want a specific version, DM me. +*If you want a specific version, create an issue.* ## Notes * Inspired by XSpotify diff --git a/SpotifyKeyDumper/Hooks.cpp b/SpotifyKeyDumper/Hooks.cpp index a1dd91c..8ad21ba 100644 --- a/SpotifyKeyDumper/Hooks.cpp +++ b/SpotifyKeyDumper/Hooks.cpp @@ -27,10 +27,20 @@ signalEmitter_v45 signalEmitter_v45_hook = nullptr; typedef void (__cdecl* keyDecoder_v47)(); keyDecoder_v47 keyDecoder_v47_hook = nullptr; +typedef int (__thiscall* startUpdate_v47)(char* This, char* newVerChars, char** updateUrlPtr, void* a4, int a5, void* a6, + int a7); +startUpdate_v47 startUpdate_v47_hook = nullptr; + +typedef void (__thiscall* uriHandler_v47)(DWORD* This, void* a2, int a3, char a4); +uriHandler_v47 uriHandler_v47_hook = nullptr; + +typedef bool (__cdecl* isValidUrl_v25)(int* urlThingArg, int a2); +isValidUrl_v25 isValidUrl_v25_hook = nullptr; + std::string authToken = std::string(); std::string keyStr = std::string(); -std::string trackUriStr = std::string(); +std::string uriStr = std::string(); __int64 newPosition = 0; bool signalled = false; int destKeyPtr = 0; @@ -124,7 +134,7 @@ int* __fastcall authToken_hook_v45(void* This, void* _EDX, int* a2) int* __fastcall openTrack_hook_v45(void* This, void* _EDX, int a2, void* a3, int a4, __int64 position, char a6, void* a7) { - std::cout << "openTrack!!!" << std::endl << std::endl; + //std::cout << "openTrack!!!" << std::endl << std::endl; return openTrack_v45_hook(This, a2, a3, a4, newPosition, a6, a7); } @@ -138,20 +148,23 @@ int* __fastcall log_hook_v45(void* This, void* _EDX, int a2, int a3, void* a4, c if (!Utils::BadPtr(logChars)) { - //std::string logStr = std::string(logChars).substr(8, 5); std::string logStr = std::string(logChars); - //std::cout << "logStr: " << logStr << std::endl; - if (logStr.length() > 32 && logStr.compare(8, 9, "track_uri") == 0) // 19 + 13 = 32 { if (logStr.compare(19, 13, "spotify:track") == 0) { //std::cout << "Track URI: " << logStr.substr(19, std::string::npos) << std::endl; - trackUriStr = logStr.substr(19, std::string::npos); + uriStr = logStr.substr(19, std::string::npos); newPosition = 0; } - // TODO + else if (logStr.length() > 34 && logStr.compare(19, 15, "spotify:episode") == 0) + { + //std::cout << "Episode URI: " << logStr.substr(19, std::string::npos) << std::endl; + uriStr = logStr.substr(19, std::string::npos); + newPosition = 0; + } + // TODO: e.g. spotify:ad:000000014990f3ec000000203522e473 //else if (logStr.compare(19, 11, "spotify:ad") == 0) // Possibly this works? /*else { @@ -165,20 +178,18 @@ int* __fastcall log_hook_v45(void* This, void* _EDX, int a2, int a3, void* a4, c return log_v45_hook(This, a2, a3, a4, classStr, a6, logThing); } -std::string lastKey = std::string(); +std::string lastUri = std::string(); void __fastcall fileIdWriter_hook_v45(void* This, void* _EDX, int* a2) { // [[ebp+8]+28] char* fileId = (char*) *(DWORD*)(a2 + 16); // 0x40 / 4 = 16 - //std::cout << "fileId: " << fileId << std::endl << std::endl; - - if (signalled && lastKey.compare(keyStr) != 0) + if (signalled && lastUri.compare(uriStr) != 0) { //std::cout << "signalled = false" << std::endl; signalled = false; - lastKey = keyStr; - std::thread t2(Utils::DownloadSong, std::string(fileId), trackUriStr, keyStr, authToken); + lastUri = uriStr; + std::thread t2(Utils::DownloadSong, std::string(fileId), uriStr, keyStr, authToken); t2.detach(); } @@ -260,6 +271,53 @@ __declspec(naked) void keyDecoder_hook_v47() } } +int __fastcall startUpdate_hook_v47(char* This, void* _EDX, char* newVerChars, char** updateUrlPtr, void* a4, int a5, + void* a6, int a7) +{ + *updateUrlPtr[0] = '\x00'; // Breaks the URL to prevent updating + + return startUpdate_v47_hook(This, newVerChars, updateUrlPtr, a4, a5, a6, a7); +} + +void __fastcall uriHandler_hook_v47(DWORD* This, void* _EDX, void* a2, int a3, char a4) +{ + int uriType = *This; + + // Check if uri type is an ad (12) + if (uriType == 12) + { + DWORD* adUri = This - 42; // 168 / 4 = 42 + char* adUriChars = (char*)*adUri; + + for (int index = 0; index < 32; index++) + adUriChars[index] = '0'; + + //std::cout << "Ad uri: spotify:ad:" << std::string((char*)*adUri, 32) << std::endl; + + return; + } + + return uriHandler_v47_hook(This, a2, a3, a4); +} + +bool __cdecl isValidUrl_hook_v25(int* urlThingArg, int arg_4) +{ + unsigned char* source = (unsigned char*)*(void**)*(urlThingArg + 1); + std::u16string u16_str(reinterpret_cast(source)); + std::string utf8Url = std::wstring_convert, char16_t>{}.to_bytes(u16_str); + + //std::cout << "URL: " << utf8Url << std::endl; + + if (utf8Url.find("doubleclick") != std::string::npos) + { + //printf("Blocked URL: %s\n", utf8Url.c_str()); + return false; + } + else + return isValidUrl_v25_hook(urlThingArg, arg_4); +} + + char* GetKeyFuncAddrV26() { BYTE ref_v19 = 0x55; @@ -330,9 +388,9 @@ void Hooks::Init() case 45: keyToLE_v28_hook = (keyToLE_v28)Utils::TrampHook32((char*)0x010CF780, (char*)keyToLE_hook_v28, 6); authToken_v45_hook = (authToken_v45)Utils::TrampHook32((char*)0x00BF75F0, (char*)authToken_hook_v45, 7); - //openTrack_v45_hook = (openTrack_v45)Utils::TrampHook32((char*)0x00CA5740, (char*)&openTrack_hook_v45, 5); - log_v45_hook = (log_v45)Utils::TrampHook32((char*)0x010F2370, (char*)&log_hook_v45, 5); - fileIdWriter_v45_hook = (fileIdWriter_v45)Utils::TrampHook32((char*)0x00CBB560, (char*)&fileIdWriter_hook_v45, + //openTrack_v45_hook = (openTrack_v45)Utils::TrampHook32((char*)0x00CA5740, (char*)openTrack_hook_v45, 5); + log_v45_hook = (log_v45)Utils::TrampHook32((char*)0x010F2370, (char*)log_hook_v45, 5); + fileIdWriter_v45_hook = (fileIdWriter_v45)Utils::TrampHook32((char*)0x00CBB560, (char*)fileIdWriter_hook_v45, 5); signalEmitter_v45_hook = (signalEmitter_v45)Utils::TrampHook32((char*)0x00B095A0, (char*)signalEmitter_hook_v45, 5); @@ -340,8 +398,8 @@ void Hooks::Init() case 46: keyToLE_v28_hook = (keyToLE_v28)Utils::TrampHook32((char*)0x010C2FB0, (char*)keyToLE_hook_v28, 6); authToken_v45_hook = (authToken_v45)Utils::TrampHook32((char*)0x00BEC8E0, (char*)authToken_hook_v45, 7); - log_v45_hook = (log_v45)Utils::TrampHook32((char*)0x010E59E0, (char*)&log_hook_v45, 5); - fileIdWriter_v45_hook = (fileIdWriter_v45)Utils::TrampHook32((char*)0x00CB00D0, (char*)&fileIdWriter_hook_v45, + log_v45_hook = (log_v45)Utils::TrampHook32((char*)0x010E59E0, (char*)log_hook_v45, 5); + fileIdWriter_v45_hook = (fileIdWriter_v45)Utils::TrampHook32((char*)0x00CB00D0, (char*)fileIdWriter_hook_v45, 5); signalEmitter_v45_hook = (signalEmitter_v45)Utils::TrampHook32((char*)0x00B02270, (char*)signalEmitter_hook_v45, 5); @@ -349,13 +407,16 @@ void Hooks::Init() case 47: keyBuffer_v47 = new char[16]; // 128 bits = 16 bytes keyToLE_v28_hook = (keyToLE_v28)Utils::TrampHook32((char*)0x010C5B00, (char*)keyToLE_hook_v47, 6); - keyDecoder_v47_hook = (keyDecoder_v47)Utils::TrampHook32((char*)0x0153148D /*0x015337CD*/, (char*)keyDecoder_hook_v47, 7 /*6*/); + keyDecoder_v47_hook = (keyDecoder_v47)Utils::TrampHook32((char*)0x0153148D, (char*)keyDecoder_hook_v47, 7); authToken_v45_hook = (authToken_v45)Utils::TrampHook32((char*)0x00BED0F0, (char*)authToken_hook_v45, 7); - log_v45_hook = (log_v45)Utils::TrampHook32((char*)0x010E8750, (char*)&log_hook_v45, 5); - fileIdWriter_v45_hook = (fileIdWriter_v45)Utils::TrampHook32((char*)0x00CB0630, (char*)&fileIdWriter_hook_v45, + log_v45_hook = (log_v45)Utils::TrampHook32((char*)0x010E8750, (char*)log_hook_v45, 5); + fileIdWriter_v45_hook = (fileIdWriter_v45)Utils::TrampHook32((char*)0x00CB0630, (char*)fileIdWriter_hook_v45, 5); signalEmitter_v45_hook = (signalEmitter_v45)Utils::TrampHook32((char*)0x00AFBB50, (char*)signalEmitter_hook_v45, 5); + //startUpdate_v47_hook = (startUpdate_v47)Utils::TrampHook32((char*)0x009FB530, (char*)startUpdate_hook_v47, 5); + //uriHandler_v47_hook = (uriHandler_v47)Utils::TrampHook32((char*)0x010A39E0, (char*)uriHandler_hook_v47, 5); + //isValidUrl_v25_hook = (isValidUrl_v25)Utils::TrampHook32((char*)0x0105CD80, (char*)isValidUrl_hook_v25, 6); break; } } \ No newline at end of file diff --git a/SpotifyKeyDumper/Utils.cpp b/SpotifyKeyDumper/Utils.cpp index 5ccde29..cc24573 100644 --- a/SpotifyKeyDumper/Utils.cpp +++ b/SpotifyKeyDumper/Utils.cpp @@ -120,134 +120,210 @@ struct SongInfo std::string title, artist, album, cover; } songInfo; +void ClearSongInfo() +{ + songInfo.title = ""; + songInfo.artist = ""; + songInfo.album = ""; + songInfo.cover = ""; +} + static const std::string songRegex = "https?:\\/\\/(?:www\.)?([-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6})" "\\b([-a-zA-Z0-9()@:%_\\+.~#?&\\/\\/=]*)"; static const std::string albumSearchPattern = "\x68\x65\x69\x67\x68\x74\x22\x20\x3A\x20\x36\x34\x30"; static const std::wstring songDirRoot = L"Downloads"; static std::wstring songDir = songDirRoot; -void Utils::DownloadSong(std::string fileId, std::string trackUri, std::string key, std::string authToken) +void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, std::string authToken) { - std::cout << "Downloading song..." << std::endl; + std::string downloadStr; + std::wstring songExtension = L".ogg"; - if (fileId.empty() || trackUri.empty() || key.empty() || authToken.empty()) + if (fileId.empty() || uri.empty() || authToken.empty()) { - std::cout << "Could not download song: missing fileId, trackUri, key, or authToken!" << std::endl; + std::cout << "Could not download song or episode: missing fileId, trackUri, or authToken!" << std::endl; return; } - // Get storage resolve from Spotify - std::string srStr = DownloadSpotifyUrl("spclient.wg.spotify.com", - "/storage-resolve/files/audio/interactive_prefetch/" + fileId + "?product=0", authToken); - - if (srStr.length() <= 5) + // Is length check even needed for compare()? + if (uri.length() > 13 && uri.compare(0, 13, "spotify:track") == 0) { - std::cout << "Error: Couldn't fetch storage resolve!" << std::endl; - return; - } + std::cout << "Downloading song..." << std::endl; - if (srStr.substr(0, 5).compare("Error") == 0) - { - std::cout << srStr << std::endl; - return; - } - - // Parse storage resolve response to get the encrypted song data's URL - std::string songHost; - std::string songPath; - try - { - std::regex re(songRegex); - std::smatch match; - if (std::regex_search(srStr, match, re) && match.size() > 1) + if (key.empty()) { - songHost = match.str(1); - songPath = match.str(2); - } - else - { - std::cout << "Error: Download URL not found" << std::endl; + std::cout << "Could not download song: missing key!"; return; } - } - catch (std::regex_error& e) - { - // Syntax error in the regular expression - std::cout << "Error: regex_error" << std::endl; - return; - } - // Download encrypted song data from Spotify - std::string songStr = DownloadSpotifyUrl(songHost, songPath, ""); + // Get storage resolve from Spotify + std::string srStr = DownloadSpotifyUrl("spclient.wg.spotify.com", + "/storage-resolve/files/audio/interactive_prefetch/" + fileId + "?product=0", authToken); - if (songStr.length() <= 6) - { - std::cout << "Error: Could not download audio!" << std::endl; - return; - } + if (srStr.length() <= 5) + { + std::cout << "Error: Couldn't fetch storage resolve!" << std::endl; + return; + } - if (songStr.substr(0, 6).compare("") == 0) - { - std::cout << "Error: " + songStr << std::endl; - return; - } - - // Decrypt encrypted song data with Tiny AES in C - struct AES_ctx ctx; - AES_init_ctx_iv(&ctx, reinterpret_cast(&key[0]), IV); - AES_CTR_xcrypt_buffer(&ctx, reinterpret_cast(&songStr[0]), songStr.size()); + if (srStr.substr(0, 5).compare("Error") == 0) + { + std::cout << srStr << std::endl; + return; + } - // Remove custom Spotify Ogg page from beginning of file - songStr = songStr.substr(songStr.find("\xFF\xFF\xFF\xFFOggS") + 4); + // Parse storage resolve response to get the encrypted song data's URL + std::string songHost; + std::string songPath; + try + { + std::regex re(songRegex); + std::smatch match; + if (std::regex_search(srStr, match, re) && match.size() > 1) + { + songHost = match.str(1); + songPath = match.str(2); + } + else + { + std::cout << "Error: Download URL not found" << std::endl; + return; + } + } + catch (std::regex_error& e) + { + // Syntax error in the regular expression + std::cout << "Error: Invalid regex!" << std::endl; + return; + } - if (!trackUri.empty()) - { + // Download encrypted song data from Spotify + downloadStr = DownloadSpotifyUrl(songHost, songPath, ""); + + if (downloadStr.length() <= 6) + { + std::cout << "Error: Could not download audio!" << std::endl; + return; + } + + if (downloadStr.substr(0, 6).compare("") == 0) + { + std::cout << "Error: " + downloadStr << std::endl; + return; + } + + // Decrypt encrypted song data with Tiny AES in C + struct AES_ctx ctx; + AES_init_ctx_iv(&ctx, reinterpret_cast(&key[0]), IV); + AES_CTR_xcrypt_buffer(&ctx, reinterpret_cast(&downloadStr[0]), downloadStr.size()); + + // Remove custom Spotify Ogg page from beginning of file + downloadStr = downloadStr.substr(downloadStr.find("\xFF\xFF\xFF\xFFOggS") + 4); + + // Download song metadata from Spotify API std::string metadata = DownloadSpotifyUrl("api.spotify.com", "/v1/tracks/" - + trackUri.substr(trackUri.find("spotify:track:") + 14), authToken); + + uri.substr(uri.find("spotify:track:") + 14), authToken); songInfo.title = strtok((char*)(metadata.substr(metadata.find("is_local") + 55)).c_str(), "\""); songInfo.artist = strtok((char*)(metadata.substr(metadata.find("name") + 9)).c_str(), "\""); songInfo.album = strtok((char*)(metadata.substr(metadata.find(albumSearchPattern) + 404)).c_str(), "\""); songInfo.cover = strtok((char*)(metadata.substr(metadata.find("height") + 30)).c_str(), "\""); - std::wstring tempDirArtist = FixPathStr(Utf8ToUtf16(songInfo.artist)); + songExtension = L".ogg"; + } + else if (uri.length() > 15 && uri.compare(0, 15, "spotify:episode") == 0) + { + std::string episodeUrl = fileId; + std::string songHost; + std::string songPath; - songDir = songDirRoot; - if (!CreateDirectoryW(songDir.c_str(), NULL) && ERROR_ALREADY_EXISTS != GetLastError()) - std::cout << "Couldn't create main downloads directory!" << std::endl; + std::cout << "Downloading episode..." << std::endl; - if (CreateDirectoryW(std::wstring(songDir + L"\\" + tempDirArtist).c_str(), NULL) - || ERROR_ALREADY_EXISTS == GetLastError()) + // Parse episode URL to separate host and path + try { - std::wstring tempDirAlbum = FixPathStr(Utf8ToUtf16(songInfo.album)); - - if (CreateDirectoryW(std::wstring(songDir + L"\\" + tempDirArtist + std::wstring(L"\\") - + tempDirAlbum).c_str(), NULL) || ERROR_ALREADY_EXISTS == GetLastError()) + std::regex re(songRegex); + std::smatch match; + if (std::regex_search(episodeUrl, match, re) && match.size() > 1) { - songDir += L"\\" + tempDirArtist + std::wstring(L"\\") + tempDirAlbum; - - std::wstring tempDirSong = FixPathStr(Utf8ToUtf16(songInfo.title)); - - std::ofstream songFileOut(songDir + L".\\" + tempDirArtist + L" - " + tempDirSong + L".ogg", - std::ios_base::binary); - songFileOut.write(songStr.c_str(), songStr.size()); - songFileOut.close(); - - std::cout << "Finished downloading: " << songInfo.artist << " - \"" << songInfo.title << "\"!" << std::endl; - return; + songHost = match.str(1); + songPath = match.str(2); } else { - std::cout << "Couldn't create album directory!" << std::endl; + std::cout << "Error: Download URL is not valid!" << std::endl; + return; } } + catch (std::regex_error& e) + { + // Syntax error in the regular expression + std::cout << "Error: Invalid regex!" << std::endl; + return; + } + + // Download encrypted song data from Spotify + downloadStr = DownloadSpotifyUrl(songHost, songPath, ""); + + // Download episode and show metadata from Spotify API + std::string metadata = DownloadSpotifyUrl("api.spotify.com", "/v1/episodes/" + + uri.substr(uri.find("spotify:episode:") + 16), authToken); + + songInfo.title = strtok((char*)(metadata.substr(metadata.find("name") + 9)).c_str(), "\""); + songInfo.artist = strtok((char*)(metadata.substr(metadata.find("publisher") + 14)).c_str(), "\""); + songInfo.album = strtok((char*)(metadata.substr(metadata.find("media_type") + 37)).c_str(), "\""); + songInfo.cover = strtok((char*)(metadata.substr(metadata.find("height") + 28)).c_str(), "\""); + + songExtension = L".mp3"; + } + + if (songInfo.title.empty() || songInfo.artist.empty() || songInfo.album.empty()) + { + std::cout << "Error: Invalid title/artist/album name!" << std::endl; + return; + } + + std::wstring tempDirArtist = FixPathStr(Utf8ToUtf16(songInfo.artist)); + + songDir = songDirRoot; + if (!CreateDirectoryW(songDir.c_str(), NULL) && ERROR_ALREADY_EXISTS != GetLastError()) + std::cout << "Couldn't create main downloads directory!" << std::endl; + + if (CreateDirectoryW(std::wstring(songDir + L"\\" + tempDirArtist).c_str(), NULL) + || ERROR_ALREADY_EXISTS == GetLastError()) + { + std::wstring tempDirAlbum = FixPathStr(Utf8ToUtf16(songInfo.album)); + + if (CreateDirectoryW(std::wstring(songDir + L"\\" + tempDirArtist + std::wstring(L"\\") + + tempDirAlbum).c_str(), NULL) || ERROR_ALREADY_EXISTS == GetLastError()) + { + songDir += L"\\" + tempDirArtist + std::wstring(L"\\") + tempDirAlbum; + + std::wstring tempDirSong = FixPathStr(Utf8ToUtf16(songInfo.title)); + + std::ofstream songFileOut(songDir + L".\\" + tempDirArtist + L" - " + tempDirSong + songExtension, + std::ios_base::binary); + songFileOut.write(downloadStr.c_str(), downloadStr.size()); + songFileOut.close(); + + std::cout << "Finished downloading: " << songInfo.artist << " - \"" << songInfo.title << "\"!" << std::endl; + + ClearSongInfo(); + return; + } else { - std::cout << "Couldn't create artist directory!" << std::endl; + std::cout << "Couldn't create album directory!" << std::endl; } - - std::cout << "Could not finish downloading song!" << std::endl; } + else + { + std::cout << "Couldn't create artist directory!" << std::endl; + } + + std::cout << "Could not finish downloading song!" << std::endl; + + ClearSongInfo(); } std::string GetLastErrorAsString() diff --git a/SpotifyKeyDumper/pch.h b/SpotifyKeyDumper/pch.h index af3b77a..68ed8b3 100644 --- a/SpotifyKeyDumper/pch.h +++ b/SpotifyKeyDumper/pch.h @@ -11,6 +11,7 @@ #pragma comment(lib, "Version.lib") #pragma comment(lib, "Wininet.lib") +#include #include #include "framework.h" #include diff --git a/screenshot_example.png b/screenshot_example.png deleted file mode 100644 index aff6914..0000000 Binary files a/screenshot_example.png and /dev/null differ