Skip to content

Instantly share code, notes, and snippets.

@Sighyu
Created December 20, 2025 03:23
Show Gist options
  • Select an option

  • Save Sighyu/b0af7349cd5783654b54778e3cfd1d89 to your computer and use it in GitHub Desktop.

Select an option

Save Sighyu/b0af7349cd5783654b54778e3cfd1d89 to your computer and use it in GitHub Desktop.
fk
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
namespace YUCP.GrooveKit.Credentials
{
public static class SpotifyCredentialManager
{
// --- Constants ---
private const string BaseUrl = "https://open.spotify.com";
private const string ClientTokenUrl = "https://clienttoken.spotify.com/v1/clienttoken";
private const string ServerTimeUrl = "https://open.spotify.com/api/server-time";
private const string TokenUrl = "https://open.spotify.com/api/token";
private const string ProfileUrl = "https://api.spotify.com/v1/me";
// This appears to be a private backend for the mod to fetch TOTP secrets
private const string SecretsUrl = "https://melody.yucp.club/secrets/dict";
private const string SecretsAuthHeader = "Bearer i0ng9l31ctCsBpcUQK3izwm2330PMKNu";
private const string CookieFileName = "sp_dc_cookie.dat";
// --- State ---
private static Dictionary<int, byte[]> _totpSecrets;
private static int _latestTotpVersion;
// --- Public API ---
public static bool LoadCookieFromVRCOSC(string vrcOscUserDataPath, out string spDcCookie)
{
return TryLoadCookie(vrcOscUserDataPath, out spDcCookie);
}
public static bool ValidateTokens(string accessToken, string clientToken, out JObject profileData)
{
return TryValidateToken(accessToken, clientToken, out profileData);
}
public static bool RefreshAccessToken(string vrcOscUserDataPath, string spDcCookie, out string newAccessToken, out string newClientToken)
{
return TryRefreshToken(vrcOscUserDataPath, spDcCookie, out newAccessToken, out newClientToken);
}
public static JObject FetchUserProfile(string accessToken)
{
return GetUserProfile(accessToken);
}
public static string DetectVRCOSCUserDataPath()
{
// Usually: %localappdata%\VRCOSC\current\user_data
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "VRCOSC", "current", "user_data");
if (Directory.Exists(path))
{
Console.WriteLine($"[SpotifyCredentials] Auto-detected VRC OSC path: {path}");
return path;
}
Console.WriteLine($"[SpotifyCredentials] Could not auto-detect VRC OSC path. Tried: {path}");
return null;
}
// --- Internal Logic ---
private static bool TryLoadCookie(string path, out string decryptedCookie)
{
decryptedCookie = null;
if (string.IsNullOrEmpty(path))
{
Console.WriteLine("[SpotifyCredentials] VRC OSC user data path is not set");
return false;
}
if (!Directory.Exists(path))
{
Console.WriteLine($"[SpotifyCredentials] VRC OSC user data directory does not exist: {path}");
return false;
}
Console.WriteLine($"[SpotifyCredentials] Loading sp_dc cookie from: {path}");
string cookieFilePath = Path.Combine(path, CookieFileName);
if (!File.Exists(cookieFilePath))
{
Console.WriteLine("[SpotifyCredentials] sp_dc cookie not found - cannot authenticate");
return false;
}
decryptedCookie = DecryptFile(cookieFilePath);
if (string.IsNullOrEmpty(decryptedCookie))
{
Console.WriteLine("[SpotifyCredentials] sp_dc cookie is empty or failed to decrypt");
return false;
}
Console.WriteLine($"[SpotifyCredentials] Loaded sp_dc cookie (length: {decryptedCookie.Length})");
return true;
}
private static bool TryValidateToken(string accessToken, string clientToken, out JObject profileData)
{
profileData = null;
if (string.IsNullOrEmpty(accessToken))
{
Console.WriteLine("[SpotifyCredentials] Cannot validate - access token is empty");
return false;
}
try
{
return ValidateTokenRequest(accessToken, out profileData);
}
catch (WebException ex)
{
Console.WriteLine($"[SpotifyCredentials] Token validation failed: {ex.Message}");
return false;
}
}
private static bool ValidateTokenRequest(string accessToken, out JObject profileData)
{
profileData = null;
using (WebClient webClient = new WebClient())
{
webClient.Headers.Add("Authorization", "Bearer " + accessToken);
webClient.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
string json = webClient.DownloadString(ProfileUrl);
profileData = JObject.Parse(json);
Console.WriteLine($"[SpotifyCredentials] Token validation successful - User: {profileData["display_name"]}");
return true;
}
}
private static bool TryRefreshToken(string userDataPath, string spDcCookie, out string accessToken, out string clientToken)
{
accessToken = null;
clientToken = null;
if (string.IsNullOrEmpty(spDcCookie))
{
Console.WriteLine("[SpotifyCredentials] Cannot refresh - sp_dc cookie is empty");
return false;
}
try
{
Console.WriteLine("[SpotifyCredentials] Refreshing tokens using sp_dc cookie...");
// 1. Ensure we have the TOTP secrets from the remote server
EnsureSecretsLoaded();
// 2. Get Spotify Server Time
long serverTime = GetSpotifyServerTime(spDcCookie);
long localTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
long serverTimeMs = serverTime * 1000L;
// 3. Generate TOTP codes
string totpCodeLocal = GenerateTotp(localTime);
string totpCodeServer = GenerateTotp(serverTimeMs);
Console.WriteLine("[SpotifyCredentials] TOTP codes generated successfully");
// 4. Request the Token
string tokenResponseJson = RequestToken(totpCodeLocal, totpCodeServer, serverTime, localTime, spDcCookie);
JObject tokenObj = JObject.Parse(tokenResponseJson);
accessToken = tokenObj["accessToken"]?.ToString();
string clientId = tokenObj["clientId"]?.ToString();
if (string.IsNullOrEmpty(accessToken))
{
Console.WriteLine("[SpotifyCredentials] Access token not found in response");
return false;
}
Console.WriteLine("[SpotifyCredentials] Access token refreshed successfully");
// 5. Get Client Token using Client ID
clientToken = FetchClientToken(clientId);
if (!string.IsNullOrEmpty(accessToken) && !string.IsNullOrEmpty(clientToken))
{
Console.WriteLine("[SpotifyCredentials] Token refresh successful!");
return true;
}
else
{
Console.WriteLine("[SpotifyCredentials] Token refresh returned invalid tokens");
return false;
}
}
catch (Exception ex)
{
Console.WriteLine($"[SpotifyCredentials] Token refresh failed: {ex.Message}\n{ex.StackTrace}");
return false;
}
}
private static long GetSpotifyServerTime(string spDcCookie)
{
using (WebClient webClient = new WebClient())
{
webClient.Headers.Add("Cookie", "sp_dc=" + spDcCookie);
webClient.Headers.Add("Origin", BaseUrl);
webClient.Headers.Add("Referer", BaseUrl);
webClient.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
string json = webClient.DownloadString(ServerTimeUrl);
long serverTime = JObject.Parse(json)["serverTime"].Value<long>();
Console.WriteLine($"[SpotifyCredentials] Server time: {serverTime}");
return serverTime;
}
}
private static string RequestToken(string totp, string totpServer, long sTime, long cTime, string spDcCookie)
{
var queryParams = new Dictionary<string, string>
{
["reason"] = "transport",
["productType"] = "web-player",
["totp"] = totp,
["totpServer"] = totpServer,
["totpVer"] = _latestTotpVersion.ToString(),
["sTime"] = sTime.ToString(),
["cTime"] = cTime.ToString(),
["buildVer"] = "web-player_8d8596d6",
["buildDate"] = "2024-11-20" // Reconstructed from array logic in original
};
string queryString = string.Join("&", queryParams.Select(kvp => $"{kvp.Key}={WebUtility.UrlEncode(kvp.Value)}"));
string url = $"{TokenUrl}?{queryString}";
using (WebClient webClient = new WebClient())
{
webClient.Headers.Add("Cookie", "sp_dc=" + spDcCookie);
webClient.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
return webClient.DownloadString(url);
}
}
private static JObject GetUserProfile(string accessToken)
{
if (string.IsNullOrEmpty(accessToken))
{
Console.WriteLine("[SpotifyCredentials] Cannot fetch profile - access token is empty");
return null;
}
try
{
using (WebClient webClient = new WebClient())
{
webClient.Headers.Add("Authorization", "Bearer " + accessToken);
webClient.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
string json = webClient.DownloadString(ProfileUrl);
JObject profile = JObject.Parse(json);
string displayName = profile["display_name"]?.ToString() ?? "Unknown";
string product = profile["product"]?.ToString() ?? "free";
Console.WriteLine($"[SpotifyCredentials] Profile fetched: {displayName} ({product})");
return profile;
}
}
catch (WebException ex)
{
HttpWebResponse response = ex.Response as HttpWebResponse;
if (response != null)
{
Console.WriteLine($"[SpotifyCredentials] Profile fetch failed with status {response.StatusCode}: {ex.Message}");
if (response.StatusCode == HttpStatusCode.Unauthorized)
{
Console.WriteLine("[SpotifyCredentials] Access token expired (401 Unauthorized)");
}
}
else
{
Console.WriteLine("[SpotifyCredentials] Profile fetch failed: " + ex.Message);
}
return null;
}
}
// --- TOTP / Secrets Logic ---
private static void EnsureSecretsLoaded()
{
if (_totpSecrets != null) return;
Console.WriteLine("[SpotifyCredentials] Downloading TOTP secrets from secrets endpoint...");
try
{
using (WebClient webClient = new WebClient())
{
webClient.Headers.Add("Authorization", SecretsAuthHeader);
string json = webClient.DownloadString(SecretsUrl);
Console.WriteLine($"[SpotifyCredentials] Raw response from secrets endpoint: {json}");
_totpSecrets = ParseSecrets(json);
if (_totpSecrets.Count == 0)
{
Console.WriteLine("[SpotifyCredentials] No TOTP secrets found in response.");
throw new InvalidOperationException("No TOTP secrets found.");
}
_latestTotpVersion = _totpSecrets.Keys.Max();
Console.WriteLine($"[SpotifyCredentials] Downloaded {_totpSecrets.Count} TOTP versions; using latest={_latestTotpVersion}");
}
}
catch (Exception ex)
{
throw new InvalidOperationException("Failed to download TOTP secrets: " + ex.Message, ex);
}
}
private static Dictionary<int, byte[]> ParseSecrets(string json)
{
var result = new Dictionary<int, byte[]>();
JObject root = JObject.Parse(json);
// Handle both structure types (nested secretDict or flat)
JObject dictToParse = root["secretDict"] as JObject ?? root;
foreach (var prop in dictToParse.Properties())
{
if (int.TryParse(prop.Name, out int version) && prop.Value is JArray bytesArray)
{
result[version] = bytesArray.Select(t => (byte)t.Value<int>()).ToArray();
}
}
return result;
}
private static string GenerateTotp(long time)
{
// Decrypt/XOR the secret using the specific key logic
byte[] secret = _totpSecrets[_latestTotpVersion];
byte[] processedSecret = ProcessSecret(secret);
string hexSecret = BytesToHexWithObfuscation(processedSecret);
byte[] keyBytes = HexToBytes(hexSecret);
// Calculate time step
ulong timeStep = (ulong)(time / 1000L / 30L); // 30 second steps
byte[] timeBytes = BitConverter.GetBytes(timeStep);
if (BitConverter.IsLittleEndian) Array.Reverse(timeBytes);
byte[] hash = ComputeHmacSha1(keyBytes, timeBytes);
return TruncateHash(hash);
}
private static byte[] ProcessSecret(byte[] input)
{
// The original logic XORed bytes with (index % 33 + 9)
return input.Select((b, i) => (byte)(b ^ (i % 33 + 9))).ToArray();
}
private static string BytesToHexWithObfuscation(byte[] input)
{
// Changed lambda parameter to 'val' and local variable to 'bytes'
string s = string.Concat(input.Select(val => val.ToString()));
byte[] bytes = Encoding.UTF8.GetBytes(s);
return BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
}
private static byte[] HexToBytes(string hex)
{
return Enumerable.Range(0, hex.Length / 2)
.Select(x => Convert.ToByte(hex.Substring(x * 2, 2), 16))
.ToArray();
}
private static byte[] ComputeHmacSha1(byte[] key, byte[] data)
{
using (HMACSHA1 hmac = new HMACSHA1(key))
{
return hmac.ComputeHash(data);
}
}
private static string TruncateHash(byte[] hash)
{
// Standard TOTP Truncation
int offset = hash[hash.Length - 1] & 0x0F;
int binary = ((hash[offset] & 0x7f) << 24) |
((hash[offset + 1] & 0xff) << 16) |
((hash[offset + 2] & 0xff) << 8) |
(hash[offset + 3] & 0xff);
int otpDigits = 6;
int otp = binary % (int)Math.Pow(10, otpDigits);
return otp.ToString($"D{otpDigits}");
}
// --- Client Token Fetcher ---
private static string FetchClientToken(string clientId)
{
try
{
Console.WriteLine("[SpotifyCredentials] Fetching client token...");
using (WebClient webClient = new WebClient())
{
SetupClientTokenHeaders(webClient);
var body = new
{
client_data = new
{
client_version = "1.2.54.124.gc8ffdbcb",
client_id = clientId,
js_sdk_data = new
{
device_brand = "unknown",
device_model = "unknown",
os = "windows",
os_version = "NT 10.0",
device_id = Guid.NewGuid().ToString(),
device_type = "computer"
}
}
};
string jsonBody = JsonConvert.SerializeObject(body);
Console.WriteLine($"[SpotifyCredentials] Client token request body: {jsonBody}");
string response = webClient.UploadString(ClientTokenUrl, "POST", jsonBody);
string token = JObject.Parse(response)["granted_token"]?["token"]?.ToString();
if (!string.IsNullOrEmpty(token))
{
Console.WriteLine("[SpotifyCredentials] Client token retrieved successfully");
return token;
}
Console.WriteLine("[SpotifyCredentials] Client token not found in response");
return null;
}
}
catch (WebException ex)
{
Console.WriteLine($"[SpotifyCredentials] Failed to get client token: {ex.Message}");
using (var stream = ex.Response?.GetResponseStream())
{
if (stream != null)
{
using (var reader = new StreamReader(stream))
{
Console.WriteLine($"Details: {reader.ReadToEnd()}");
}
}
}
return null;
}
}
private static void SetupClientTokenHeaders(WebClient client)
{
client.Headers.Add("Accept", "application/json");
client.Headers.Add("Accept-Language", "en-US,en;q=0.9");
client.Headers.Add("Content-Type", "application/json");
client.Headers.Add("Sec-CH-UA", "\"Microsoft Edge\";v=\"131\", \"Chromium\";v=\"131\", \"Not_A Brand\";v=\"24\"");
client.Headers.Add("Sec-CH-UA-Mobile", "?0");
client.Headers.Add("Sec-CH-UA-Platform", "\"Windows\"");
client.Headers.Add("Sec-Fetch-Dest", "empty");
client.Headers.Add("Sec-Fetch-Mode", "cors");
client.Headers.Add("Sec-Fetch-Site", "same-site");
client.Headers.Add("Referer", "https://accounts.spotify.com/en/login?allow_password=1");
client.Headers.Add("Referrer-Policy", "strict-origin-when-cross-origin");
}
// --- Encryption/Decryption (DPAPI) ---
private static string DecryptFile(string filePath)
{
try
{
byte[] encryptedData = File.ReadAllBytes(filePath);
byte[] decryptedData = UnprotectData(encryptedData);
return Encoding.UTF8.GetString(decryptedData);
}
catch (Exception ex)
{
Console.WriteLine($"[SpotifyCredentials] Failed to decrypt {filePath}: {ex.Message}");
return null;
}
}
// Native DPAPI Wrappers
[DllImport("crypt32.dll", SetLastError = true, CharSet = CharSet.Auto)]
private static extern bool CryptUnprotectData(ref DATA_BLOB pDataIn, string szDataDescr, ref DATA_BLOB pOptionalEntropy, IntPtr pvReserved, IntPtr pPromptStruct, uint dwFlags, ref DATA_BLOB pDataOut);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct DATA_BLOB
{
public int cbData;
public IntPtr pbData;
}
private static byte[] UnprotectData(byte[] data)
{
DATA_BLOB inBlob = new DATA_BLOB();
DATA_BLOB outBlob = new DATA_BLOB();
try
{
inBlob.cbData = data.Length;
inBlob.pbData = Marshal.AllocHGlobal(data.Length);
Marshal.Copy(data, 0, inBlob.pbData, data.Length);
DATA_BLOB entropy = default;
if (CryptUnprotectData(ref inBlob, null, ref entropy, IntPtr.Zero, IntPtr.Zero, 0, ref outBlob))
{
byte[] result = new byte[outBlob.cbData];
Marshal.Copy(outBlob.pbData, result, 0, outBlob.cbData);
return result;
}
else
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
finally
{
if (inBlob.pbData != IntPtr.Zero) Marshal.FreeHGlobal(inBlob.pbData);
if (outBlob.pbData != IntPtr.Zero) Marshal.FreeHGlobal(outBlob.pbData);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment