Add support for podcasts; fix duplicate checker; add more error handling with downloading; added experimental code (disabled); README changes

This commit is contained in:
_ 2020-11-30 18:03:51 -07:00
parent e45143da82
commit 2ebf25b1b0
5 changed files with 271 additions and 129 deletions

View File

@ -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

View File

@ -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<const char16_t*>(source));
std::string utf8Url = std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, 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;
}
}

View File

@ -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("<HTML>") == 0)
{
std::cout << "Error: " + songStr << std::endl;
return;
}
if (srStr.substr(0, 5).compare("Error") == 0)
{
std::cout << srStr << std::endl;
return;
}
// Decrypt encrypted song data with Tiny AES in C
struct AES_ctx ctx;
AES_init_ctx_iv(&ctx, reinterpret_cast<const uint8_t*>(&key[0]), IV);
AES_CTR_xcrypt_buffer(&ctx, reinterpret_cast<uint8_t*>(&songStr[0]), songStr.size());
// 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;
}
// Remove custom Spotify Ogg page from beginning of file
songStr = songStr.substr(songStr.find("\xFF\xFF\xFF\xFFOggS") + 4);
// Download encrypted song data from Spotify
downloadStr = DownloadSpotifyUrl(songHost, songPath, "");
if (!trackUri.empty())
{
if (downloadStr.length() <= 6)
{
std::cout << "Error: Could not download audio!" << std::endl;
return;
}
if (downloadStr.substr(0, 6).compare("<HTML>") == 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<const uint8_t*>(&key[0]), IV);
AES_CTR_xcrypt_buffer(&ctx, reinterpret_cast<uint8_t*>(&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()

View File

@ -11,6 +11,7 @@
#pragma comment(lib, "Version.lib")
#pragma comment(lib, "Wininet.lib")
#include <codecvt>
#include <cstdint>
#include "framework.h"
#include <fstream>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 842 KiB