Open jasonful opened 3 years ago
I've just ran into this issue as well (stopping my 2+ years of work towards my client in its tracks).
I'm inspecting the source code to see how these value are generated - if you want to, you can use my tool to download the sources (the download button is in the top right): https://toolbox.epimetheus.tk/#source_code
There seem to be no significant Web app source changes between version 1.153.0 (the last version I have before the change) and the latest 1.158.1, suggesting that this was a server-side change that's been long coming.
These new properties seem to be used for bot mitigation, in a new feature referred to as "Detector" internally.
There are references to White Ops.
The White Ops report is embedded into the login request - that's what those values are for.
The values seem to be generated by a script called pagespeed.js
, which is downloaded from a URL generated with a few properties. Here's one:
Unfortunately, the script is obfuscated...
Note the "hoplon" subdomain - a Hoplon is a Greek shield.
Another script is accessed by the script linked above: https://s.hoplon.pandora.com/sri/config.json
This may be the end for unofficial REST API usage; the bot detection seems to be highly reliant on the browser environment, and there aren't even public SDKs available.
Is the JSON API's authToken compatible with the REST API? If so, perhaps the initial login can be done with it, as this bot detection seems to be used only for login.
@hacker1024 Please edit your initial post instead of posting multiple times in a row.
Looking at https://github.com/PromyLOPh/pianobar/issues/236 this was probably overdue. Pandora never really liked 3rd party developers using their web API’s.
Can I remove the documentation for that API from this repository?
Can I remove the documentation for that API from this repository?
It looks like the JSON API authToken is actually compatible with the REST API! That means existing software can use the JSON API for logging in and no other code needs to be updated. I think it's worth keeping the REST documentation up, as it's still usable in this way.
so how can we login pandora?
@GodStar88 Use the JSON API to obtain the userAuthToken
, which can be used as the authToken
with the REST API.
thank you for your reply, I tried that, but i can't login pandora
@GodStar88 what's going wrong exactly? Can you share your code?
I tried postman, I can't login
There are 3 new fields for authentication. If they are missing, the login fails as if it's invalid credentials.
OZ_DT
and OZ_TC
are retrieved from a JSON response obtained via GET https://s.hoplon.pandora.com/sri/config.json?dt=5222681607985354683000&psv=4.80.9&ci=522268&spa=1&pd=acc&mo=2&di=pandora.com&si=SXMP&ui=985753384
Where:
Repeatedly using the same Hoplon URL parameters seems to work fine, although I am suspicious this may change as the web client version number changes.
The response from Hoplon is malformed JSON; string substitute "\\x26" -> "&" and "\\x3D" -> "=" to make it valid (These strings are presented here as quoted for C/C++; i.e., these are 4-character strings starting with a single backslash). In the resulting JSON, members ozoki_dt
and ozoki_tc
later become the OZ_DT
and OZ_TC
parameters.
OZ_SG
is the troublesome one. As mentioned by hacker1024, the client logs a bunch of internal event details into a structure, and when initiating the login, serializes it with JSON. It then encrypts it, and the resulting string is sent as OZ_SG
.
The encryption is an RC4 variant bastardized for ASCII.
OZ_TC
is used as a key. The algorithm starts with h=341005
, and each time a message is generated, h
is translated to a textual representation and prepended to both the key and encrypted output. h
is then bumped to a new value for the message.
The encryption map is then constructed, and characters in the input are then encrypted one at a time. There's some funky stuff to deal with Unicode characters, reminiscent of UTF-8 encoding, that encodes them as multiple bytes. For reference, I've included a C++ implementation of this encryption below.
class Encryptor {
using Map = int8_t [95];
using NumericSalt = uint32_t;
using MessageType = std::string; // Should be std::wstring to handle Unicode characters
NumericSalt salt;
Map starter_map;
std::string makeAddedSalt() {
NumericSalt n = salt;
salt = salt + 2654435769 & 1048575;
std::string text_salt;
for (int e = 4; e > 0; e--) {
text_salt.push_back (48 + (31 & n));
n >>= 5;
}
return text_salt;
}
public:
Encryptor (NumericSalt start_salt = 27182818) : salt (start_salt) {
int m = 51;
int v = 44;
do {
starter_map [m] = v;
v = (v + 88) % 95;
m = (m + 62) % 95;
} while (m != 51);
}
std::string operator() (const std::string &input_oz_salt, const MessageType &message) {
const std::string extra_salt = makeAddedSalt();
const std::string salt = extra_salt + input_oz_salt;
// Initialize, then shuffle shit in u
Map u;
memcpy (u, starter_map, sizeof (starter_map));
for (int v = 95, m = 94; m >= 0; m--) {
v = (v + u [m] + salt [m % salt.length()]) % 95;
std::swap (u [m], u [v]);
}
std::string encrypted;
auto append_encrypted_char ([&encrypted, &u, m = 0, v = 0] (std::char_traits <MessageType>::int_type ch) mutable -> void {
m = (m + 1) % 95;
v = (v + u [m]) % 95;
std::swap (u [m], u [v]);
ch += u [(u [m] + u [v]) % 95];
if (ch > 126) {
ch -= 95;
}
encrypted.push_back (ch);
});
for (auto ch : message) {
if ((ch < 32 || ch > 125)) {
// non-ascii; expand to multi-bytes as needed
append_encrypted_char (126);
if (ch > 2047) {
append_encrypted_char (80 + (ch >> 11));
}
append_encrypted_char (48 + (ch >> 6 & 31));
append_encrypted_char (48 + (63 & ch));
} else {
append_encrypted_char (ch);
}
}
return extra_salt + encrypted;
}
};
...
Encryptor encryptor (341005); // That's the initial "h"
std::string oz_dt = encryptor (oz_tc, message);
@GodStar88 This whole system is designed to block bots. I think you're out of luck.
Any update on this? I've been doing research as well and here are a few additional notes. As noted, OZ_SG is an encrypted JSON encoded array of messages, including various data from browser information to events. OZ_TC is used for the encryption. OZ_TC and OZ_DT come from a call to /config.json.
I've captured the unencrypted values of OZ_SG for several logins, some manual and some automated via selenium/chromium. Of course, the manual logins are successful while the automated ones are not. After looking through OZ_SG, the only thing that strikes me as a potential bot indicator are document event logs that are stored, which hold things like keystrokes and other page actions. That being said, if that was all there was to it, then you should be able to intercept the function to generate OZ_SG before encryption takes place and replace the messages with those captured from a valid / human session.
However, that doesn't seem to work. I'm thinking one of two things are happening (possibly more).
1) OZ_DT stores contextually relevant data that somehow affects the login process more than simply providing the value given by /config.json. This thread doesn't include much information on OZ_DT and what it's purpose is, so additional research may be needed there
2) Data that is sent during the /postback requests to s.hoplon.pandora.com changes what the remote end expects. In other words, by altering OZ_SG the way I mentioned earlier, it may cause a mismatch on information that was already sent
In summary, I'm trying to identify the true purpose of OZ_DT and how it is used. My original inclination was that it is a base64 encoded cryptographic hash of the config.json page (since config.json is dynamically loaded by pagespeed.js). As this data and OZ_TC change on each call to config.json, it could be that OZ_TC is used as a key for this hash. I think OZ_DT may hold some secrets in understanding what is happening.
I'm also interested in knowing why replacing (the unencrypted) OZ_SG with values from a valid previous session isn't working. Timing could be a factor. Also, it's very clear that the combination of username, password, OZ_TC, OZ_DT, and OZ_SG is a one-time-use credential. Resubmitting previously accepted values doesn't work.
Also, it's very clear that the combination of username, password, OZ_TC, OZ_DT, and OZ_SG is a one-time-use credential. Resubmitting previously accepted values doesn't work.
I copied the OZ_TC, OZ_DT, and OZ_SG values from an intercepted request into my code and managed to log in with the same credentials, but not with another account.
I think I missed an email where someone asked for an example of using JSON authentication to obtain a token to be used with the REST API, to be used with CURL. I can't find that message again, but I'll respond here nonetheless:
Logging in with the JSON API is a multi-step process, requiring partner authentication and sync-time handling. I can use my WIP Dart package to build a CLI tool that takes credentials and spits out an authentication token, if that's helpful to anyone? Dart can compile directly to machine code, as well as JavaScript.
@hacker1024 I was the one asking about that. A tool as you describe would certainly be useful.
I can use my WIP Dart package to build a CLI tool that takes credentials and spits out an authentication token, if that's helpful to anyone? Dart can compile directly to machine code, as well as JavaScript.
This would be a godsend if you're able :)
@H0r53 @FireController1847 Here you go: https://github.com/EpimetheusMusicPlayer/pandora_authenticator Binaries are in the releases.
Sweet, thanks a ton. I managed to get the JSON endpoint up and running after quite a lot of trail and error. It appears as though the REST API does not accept the userAuthToken achieved via the JSON API. I'm not sure why that is the case so this will be super helpful
I used the JSON API for the tool, but it worked for the REST API for me...
I used the JSON API for the tool, but it worked for the REST API for me...
Really? Maybe I was doing something wrong, I'll take another shot at it tonight.
@hacker1024 So I just took another shot at it, and it appears that the access token that I am getting through the JSON api is invalid for use with the REST api, but yours is not. What partner are you using for authentication? At the moment I am using IOS because that is the only one I could get the station.getPlaylist
method to work on JSON, all the other ones failed.. The authtokens I receive are a lot shorter than yours for some reason as well. The way it is not working is it responds with "Internal server error" and a code of zero when attempting /v1/station/getStations. I haven't tried any other endpoints, but like I said it works with yours so I am not sure what could be going on.
@FireController1847 I'm using Android, and I've also been able to use the old credentials I mentioned in #48. I'm not sure why your tokens are shorter, That's interesting. Could you perhaps make a new account, and provide the username, password, and an example authtoken?
Sure thing. I've been using a testing account for all of this so I have no problem sharing these, I'll delete them once you're done working with them. I switched to using the same android one as you and still cannot use station.getPlaylist
and it is still considered invalid for REST.
Username: [REDACTED]
Password: [REDACTED]
AuthToken: [REDACTED]
I will note, now that I've switched to Android the auth tokens look a lot more similar now.
Interesting - that token does in fact give me the error you described. It's missing a =
at the end, but that actually doesn't matter anyway as the backend can decode it just fine. I'm assuming that was just a typo.
Can you try this one: VIH3TsTsbIl6tFZQEz8DjhbTQ9rp650028pQ7q/ttfsbQI7v7N5rgEYw==
Strange, the one you provided me has worked. Also, the second equal at the end was not a typo that is how I have been receiving them, I find it strange that you are receiving them differently than me haha. Maybe we should continue this discussion on a platform like Discord? If you want, of course. My username is FireController1847#3577
Sure thing. @PromyLOPh, could you also maybe enable GitHub discussions?
@hacker1024 Yeah, done. Please move discussions there.
@PromyLOPh you provided a C++ implementation of the encryption - would it be possible to provide the corresponding decryption routine?
@H0r53 Nope, not my code, credit goes to @perette
Apologies, the question was intended for @perette
@H0r53 Sorry, I have no plans to implement the decryption. I am curious what you need it for? With a breakpoint set in the JavaScript in the right place, you can retrieve the unencrypted messages using a browser's inspection window.
@perette let's say I was limited to intercepting / forwarding requests without the luxury of a browser's devtools. With decryption it would become easier to recover messages, adjust, and re-encrypt.
I'm a little bit late to the party, but here are some things I've noticed.
PAGESPEED_VERSION
are still referenced, and the url is still accessible), and it seems like the new(?) login-related files are clear.js and main.jspostback
url, then the result is encoded again before being sent to the login url.
Up until yesterday, both the pandora.com web site and my own code were using the REST API to login with no problem, sending 4 parameters (existingAuthToken, username, password, keepLoggedIn). But now the web site is sending three more parameters named OZ_TC, OZ_DT, OZ_SG. The contents are encoded or encrypted. Without those 3 parameters, my code is now getting back: {"message":"Invalid username and/or password","errorCode":0,"errorString":"AUTH_INVALID_USERNAME_PASSWORD"}
Anyone know what those parameters are?