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.@hacker1024, @r0wanda
I'm late to the game but I'm very interested in adding playlist support to pianobar (https://github.com/PromyLOPh/pianobar/issues/656).
I been sniffing the REST API of the web app and I think I've figured out what I need to do, but have no idea how do proceed using the JSON API.
What is the status of logging in using the RESP API 2024, is it hopeless?
Is the JSON API's authToken still compatible with the REST API?
It seems like all new Pandora related development stopped 3 years ago?
@H0r53 re: providing decryption: No. Although it could potentially be helpful in working with the REST API, I've migrated back to the JSON API where it's not needed. Also, no time, and I don't have the cryptographic knowledge without a bunch of brushing up. It is some variant of RC4, so perhaps you could adapt or crib from an RC4 decrypt implementation?
@skiphansen JSON documentation is here. There are implementations in pianobar
/libpiano
and in pianod2
for reference and/or cookbooking. If you need something more, can you be more specific where you're stuck? The upside is JSON seems stable, REST in flux; the downside, I don't think JSON supports new features, thus no new development to integrate with it. Pandora has a partner program that offers a documented API, probably REST. You could try there—though it seems likely there are NDAs that make open-source implementations impossible, I've not confirmed it.
@perette Thanks for the quick response!
Where I'm stuck is that I haven't been able to find any documentation on how to retrieve a list of playlists using the JSON API.
From looking at some of @hacker1024 dart code I think it's playlists.v7.getTracks but when I tired to guess what the format of the parameters is I just back the super helpful response:
{"stat":"fail","code":0,"message":"Request to backend Service Failed"}
My request was:
{
"pandorId": "PL:76913039533692130:ZZZ",
"limit": 100,
"offset": 0,
"annotationLimit": 100,
"playlistVersion": null,
"userAuthToken": "XXX",
"syncTime": 1729176380
}
Where XXX was the userAuthToken obtained by pianobar and ZZZ is my listenerid.
I could create the decryption routine but I don't really care to at this point. It's been a few years since I've been interested in this.
Regarding the question "How can pandora tell if a request is from an actual browser if OZ_SG is not sending any data? It must be through OZ_TC/OZ_DT" - there are several ways that the backend can detect if a request is sent from an actual browser, including device fingerprinting, TLS fingerprinting and more.
@skiphansen - it should be noted that Pandora does not want people to automate login, streaming, or any other bot activity. This repo is not an official Pandora API Doc. To bypass the security measures in place you will need to hack your way through them. This is also a public forum so Pandora is likely to be monitoring the discussion at this point. Any solutions shared would be easy for them to react to.
@H0r53 I understand this repo isn't official.
My interest is in adding playlist support to pianobar not creating a new app. I know that pianobar isn't an officially authorized app, but it seems pretty clear that Pandora is tolerating it to some extent considering how long it's been around and some change log comments I've seen in other programs where they have changed changed their user agent to copy pianobar's in order to login. If anything since I believe playlists are a feature only supported for paid accounts if anything supporting them might cause a few uses to upgrade to a subscription.
It would be easier to use the JSON API, but I can't find any documentation on how to use the JSON API to list Pandora playlists.
I have experience reverse engineering things by protocol sniffing, but I have zero experience with reverse engineering APKs, hence that's the approach I've taken so far. I think I know how to get playlists from the REST API and hence my interest.
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?