diff --git a/SpotifyKeyDumper/Utils.cpp b/SpotifyKeyDumper/Utils.cpp index 70fc47d..27a7a2d 100644 --- a/SpotifyKeyDumper/Utils.cpp +++ b/SpotifyKeyDumper/Utils.cpp @@ -2,9 +2,15 @@ #include "Utils.h" #include "tiny-AES-c/aes.h" -const uint8_t IV[] = { 0x72, 0xE0, 0x67, 0xFB, 0xDD, 0xCB, 0xCF, 0x77, 0xEB, 0xE8, 0xBC, 0x64, 0x3F, 0x63, 0x0D, 0x93 }; -const std::string urlRegex = "https?:\\/\\/(?:www\.)?([-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6})" +constexpr BYTE IV[] = {0x72, 0xE0, 0x67, 0xFB, 0xDD, 0xCB, 0xCF, 0x77, 0xEB, 0xE8, 0xBC, 0x64, 0x3F, 0x63, 0x0D, 0x93}; +constexpr char podcastKey[] = "\xde" "\xad" "\xbe" "\xef" "\xde" "\xad" "\xbe" "\xef" "\xde" "\xad" "\xbe" "\xef" "\xde" +"\xad" "\xbe" "\xef"; +constexpr BYTE nullKey[] = "\0" "\0" "\0" "\0" "\0" "\0" "\0" "\0" "\0" "\0" "\0" "\0" "\0" "\0" "\0" "\0"; + +static const std::string urlRegex = "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"; bool Utils::Detour32(char* src, char* dst, const intptr_t len) { @@ -254,55 +260,62 @@ int GetUrlNum(int quality) } } -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 uri, std::string key, std::string authToken, int quality) +std::string AttemptDecryption(std::string data, std::string key) { - std::string downloadStr; - std::wstring songExtension = L".ogg"; + // Decrypt encrypted 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(&data[0]), data.size()); - SongInfo* songInfo = new SongInfo(); - /*songInfo->title = ""; - songInfo->artist = ""; - songInfo->album = ""; - songInfo->coverUrl = ""; - songInfo->fileType = FileType::OGG;*/ + // Check if decrypted song data has valid header (assume Ogg) + if (data.length() < 4 || data.compare(0, 4, "OggS") != 0) + return "Error: Not a valid Ogg file after decryption! Please try again"; - if (fileId.empty() || uri.empty() || authToken.empty()) + return data; +} + +std::string DownloadAudioData(std::string fileId, std::string uri, std::string key, std::string authToken, int quality) +{ + std::string audioData; + std::string songHost, songPath; + bool isSpotifyHosted; + + // Determine if we need to decrypt the download by parsing fileId for a URL + try { - std::cout << "Could not download song or episode: missing fileId, trackUri, or authToken!" << std::endl; - return; + std::regex re(urlRegex); + std::smatch match; + if (std::regex_search(fileId, match, re) && match.size() > 1) + { + // fileId is valid URL (not hosted by Spotify) + isSpotifyHosted = false; + songHost = match.str(1); + songPath = match.str(2); + } + else + { + // fileId is not valid URL (hosted by Spotify) + isSpotifyHosted = true; + } + } + catch (std::regex_error& e) + { + // Syntax error in the regular expression + return "Error: Invalid regex while parsing fileID!"; } - // Is length check even needed for compare()? - if (uri.length() > 13 && uri.compare(0, 13, "spotify:track") == 0) + if (isSpotifyHosted) { - std::cout << "Downloading song..." << std::endl; - - if (key.empty()) - { - std::cout << "Could not download song: missing key!"; - return; - } - // Get storage resolve from Spotify std::string urlNum = std::to_string(GetUrlNum(quality)); - std::string srStr = DownloadSpotifyUrl("spclient.wg.spotify.com", + std::string srStr = Utils::DownloadSpotifyUrl("spclient.wg.spotify.com", "/storage-resolve/files/audio/interactive_prefetch/" + fileId + "?product=0", authToken); //"/storage-resolve/v2/files/audio/interactive/" + urlNum + "/" + fileId + "?product=0", authToken); if (srStr.length() <= 5) - { - std::cout << "Error: Couldn't fetch storage resolve: (" + srStr + ")"<< std::endl; - return; - } - - if (srStr.substr(0, 5).compare("Error") == 0) - { - std::cout << srStr << std::endl; - return; - } + return "Error: Couldn't fetch storage resolve: (" + srStr + ")"; + else if (srStr.substr(0, 5).compare("Error") == 0) + return srStr; // Parse storage resolve response to get the encrypted song data's URL std::string songHost; @@ -318,51 +331,105 @@ void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, s } else { - std::cout << "Error: Download URL not found" << std::endl; - return; + return "Error: Download URL not found"; } } catch (std::regex_error& e) { // Syntax error in the regular expression - std::cout << "Error: Invalid regex!" << std::endl; - return; + return "Error: Invalid regex!"; } - // Download encrypted song data from Spotify - downloadStr = DownloadSpotifyUrl(songHost, songPath, ""); + // Download encrypted audio data from Spotify + audioData = Utils::DownloadSpotifyUrl(songHost, songPath, ""); - if (downloadStr.length() <= 6) + // Decrypt encrypted audio data using key + std::string decAudioData = AttemptDecryption(audioData, key); + + if (decAudioData.compare(0, 5, "Error") == 0) { - std::cout << "Error: Could not download audio!" << std::endl; - return; + // Only try again on podcasts + if (uri.length() > 15 && uri.compare(0, 15, "spotify:episode") == 0) + { + std::cout << "Podcast key: " << Utils::HexString((BYTE*)&podcastKey, 16) << std::endl; + std::string podcastKeyStr = std::string(podcastKey, 16); + + std::cout << "podcastKeyStr = " << podcastKeyStr << std::endl; + + // Try decryption again with podcast key + decAudioData = AttemptDecryption(audioData, podcastKeyStr); + + if (decAudioData.compare(0, 5, "Error") == 0) + return "Error: Could not properly decrypt podcast data (try downloading again)!"; + } + else + return "Error: Could not properly decrypt song data (try downloading again)!"; } - if (downloadStr.compare(0, 6, "") == 0) + // Remove custom Spotify Ogg page from beginning of file and return audio data + audioData = decAudioData.substr(audioData.find("\xFF\xFF\xFF\xFFOggS") + 4); + } + else + { + // Parse episode URL (fileId) to separate host and path + try { - std::cout << "Error: " + downloadStr << std::endl; - return; + std::regex re(urlRegex); + std::smatch match; + if (std::regex_search(fileId, match, re) && match.size() > 1) + { + songHost = match.str(1); + songPath = match.str(2); + } + else + { + return "Error: Download URL is not valid!"; + } } - else if (downloadStr.compare(0, 5, "Error") == 0) + catch (std::regex_error& e) { - std::cout << downloadStr << std::endl; - return; + // Syntax error in the regular expression + return "Error: Invalid regex!"; } - // 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()); + // Download episode data from URL + audioData = Utils::DownloadSpotifyUrl(songHost, songPath, ""); + } - // Check if decrypted song data has valid header - if (downloadStr.length() < 4 || downloadStr.compare(0, 4, "OggS") != 0) + return audioData; +} + +static std::wstring songDir = songDirRoot; +void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, std::string authToken, int quality) +{ + std::string downloadStr; + std::wstring songExtension = L".ogg"; + + SongInfo* songInfo = new SongInfo(); + + if (fileId.empty() || uri.length() < 13 || authToken.empty()) + { + std::cout << "Could not download song or episode: missing fileId, trackUri, or authToken!" << std::endl; + delete songInfo; + return; + } + + std::cout << "fileId = " + fileId << std::endl; + std::cout << "uri = " + uri << std::endl; + std::cout << "key = " + key << std::endl; + std::cout << "keyHex = " + Utils::HexString((BYTE*)&key[0], 16) << std::endl; + + if (uri.compare(0, 13, "spotify:track") == 0) + { + // Not an episode which means no predictable key + if (memcmp(&key[0], podcastKey, 16) == 0 || memcmp(&key[0], nullKey, 16) == 0) { - std::cout << "Error: Could not properly decrypt song (try downloading again)!" << std::endl; + std::cout << "Error: Key is of the wrong type! Please try again." << std::endl; + delete songInfo; return; } - // Remove custom Spotify Ogg page from beginning of file - downloadStr = downloadStr.substr(downloadStr.find("\xFF\xFF\xFF\xFFOggS") + 4); + std::cout << "Downloading song..." << std::endl; // Download song metadata from Spotify API std::string metadata = DownloadSpotifyUrl("api.spotify.com", "/v1/tracks/" @@ -390,46 +457,10 @@ void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, s } else if (uri.length() > 15 && uri.compare(0, 15, "spotify:episode") == 0) { - std::string episodeUrl = fileId; - std::string songHost; - std::string songPath; + std::string songHost, songPath; std::cout << "Downloading episode..." << std::endl; - // Parse episode URL to separate host and path - try - { - std::regex re(urlRegex); - std::smatch match; - if (std::regex_search(episodeUrl, match, re) && match.size() > 1) - { - songHost = match.str(1); - songPath = match.str(2); - } - else - { - 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 episode data from Spotify - downloadStr = DownloadSpotifyUrl(songHost, songPath, ""); - - // TODO: MP3 files appear to have a variety of headers, not sure this is 100% reliable - // Check if downloaded episode data has valid header - /*if (downloadStr.length() < 3 || downloadStr.compare(0, 3, "ID3") != 0) - { - std::cout << "Error: Could not properly download podcast episode (try downloading again)!" << std::endl; - return; - }*/ - // 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); @@ -438,8 +469,37 @@ void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, s 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->coverUrl = strtok((char*)(metadata.substr(metadata.find("height\" :") + 28)).c_str(), "\""); - songInfo->fileType = FileType::MP3; + } + else + { + std::cout << "Error: Invalid URI!" << std::endl; + delete songInfo; + return; + } + downloadStr = DownloadAudioData(fileId, uri, key, authToken, quality); + + if (downloadStr.length() < 6) + { + std::cout << "Error: Could not download audio!" << std::endl; + delete songInfo; + return; + } + else if (downloadStr.compare(0, 6, "") == 0) + { + std::cout << "Error: " + downloadStr << std::endl; + delete songInfo; + return; + } + else if (downloadStr.compare(0, 5, "Error") == 0) + { + std::cout << downloadStr << std::endl; + delete songInfo; + return; + } + else if (downloadStr.compare(0, 3, "ID3") == 0) + { + songInfo->fileType = FileType::MP3; songExtension = L".mp3"; } @@ -450,6 +510,7 @@ void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, s return; } + // TODO: Separate filesystem logic into another function std::wstring tempDirArtist = FixPathStr(Utf8ToUtf16(songInfo->artist)); songDir = songDirRoot; @@ -490,14 +551,12 @@ void Utils::DownloadSong(std::string fileId, std::string uri, std::string key, s return; } else - { std::cout << "Couldn't create album directory!" << std::endl; - } } else - { std::cout << "Couldn't create artist directory!" << std::endl; - } + + delete songInfo; } std::string Utils::DownloadSpotifyUrl(std::string host, std::string path, std::string authToken)