manage Spotify credentials

This commit is contained in:
philippe44
2023-09-27 19:36:38 -07:00
parent 506a5aaf7a
commit 5068309d25
10 changed files with 193 additions and 41 deletions

View File

@@ -13,14 +13,14 @@ esp_err_t store_nvs_value_len(nvs_type_t type, const char *key, void * data, siz
esp_err_t store_nvs_value(nvs_type_t type, const char *key, void * data);
esp_err_t get_nvs_value(nvs_type_t type, const char *key, void*value, const uint8_t buf_size);
void * get_nvs_value_alloc(nvs_type_t type, const char *key);
void * get_nvs_value_alloc_for_partition(const char * partition,const char * namespace,nvs_type_t type, const char *key, size_t * size);
esp_err_t erase_nvs_for_partition(const char * partition, const char * namespace,const char *key);
esp_err_t store_nvs_value_len_for_partition(const char * partition,const char * namespace,nvs_type_t type, const char *key, const void * data,size_t data_len);
void * get_nvs_value_alloc_for_partition(const char * partition,const char * ns,nvs_type_t type, const char *key, size_t * size);
esp_err_t erase_nvs_for_partition(const char * partition, const char * ns,const char *key);
esp_err_t store_nvs_value_len_for_partition(const char * partition,const char * ns,nvs_type_t type, const char *key, const void * data,size_t data_len);
esp_err_t erase_nvs(const char *key);
void print_blob(const char *blob, size_t len);
const char *type_to_str(nvs_type_t type);
nvs_type_t str_to_type(const char *type);
esp_err_t erase_nvs_partition(const char * partition, const char * namespace);
esp_err_t erase_nvs_partition(const char * partition, const char * ns);
void erase_settings_partition();
#ifdef __cplusplus
}

View File

@@ -131,6 +131,7 @@ static struct {
struct arg_str *deviceName;
// struct arg_int *volume;
struct arg_int *bitrate;
struct arg_int *zeroConf;
struct arg_end *end;
} cspot_args;
static struct {
@@ -656,6 +657,9 @@ static int do_cspot_config(int argc, char **argv){
if(cspot_args.bitrate->count>0){
cjson_update_number(&cspot_config,cspot_args.bitrate->hdr.longopts,cspot_args.bitrate->ival[0]);
}
if(cspot_args.zeroConf->count>0){
cjson_update_number(&cspot_config,cspot_args.zeroConf->hdr.longopts,cspot_args.zeroConf->ival[0]);
}
if(!nerrors ){
fprintf(f,"Storing cspot parameters.\n");
@@ -668,6 +672,9 @@ static int do_cspot_config(int argc, char **argv){
if(cspot_args.bitrate->count>0){
fprintf(f,"Bitrate changed to %u\n",cspot_args.bitrate->ival[0]);
}
if(cspot_args.zeroConf->count>0){
fprintf(f,"ZeroConf changed to %u\n",cspot_args.zeroConf->ival[0]);
}
}
if(!nerrors ){
fprintf(f,"Done.\n");
@@ -853,6 +860,10 @@ cJSON * cspot_cb(){
if(cspot_values){
cJSON_AddNumberToObject(values,cspot_args.bitrate->hdr.longopts,cJSON_GetNumberValue(cspot_values));
}
cspot_values = cJSON_GetObjectItem(cspot_config,cspot_args.zeroConf->hdr.longopts);
if(cspot_values){
cJSON_AddNumberToObject(values,cspot_args.zeroConf->hdr.longopts,cJSON_GetNumberValue(cspot_values));
}
cJSON_Delete(cspot_config);
return values;
@@ -1286,6 +1297,7 @@ static void register_known_templates_config(){
static void register_cspot_config(){
cspot_args.deviceName = arg_str1(NULL,"deviceName","","Device Name");
cspot_args.bitrate = arg_int1(NULL,"bitrate","96|160|320","Streaming Bitrate (kbps)");
cspot_args.zeroConf = arg_int1(NULL,"zeroConf","0|1","Force use of ZeroConf");
// cspot_args.volume = arg_int1(NULL,"volume","","Spotify Volume");
cspot_args.end = arg_end(1);
const esp_console_cmd_t cmd = {

View File

@@ -30,10 +30,16 @@
#include "cspot_private.h"
#include "cspot_sink.h"
#include "platform_config.h"
#include "nvs_utilities.h"
#include "tools.h"
static class cspotPlayer *player;
static const struct {
const char *ns;
const char *credentials;
} spotify_ns = { .ns = "spotify", .credentials = "credentials" };
/****************************************************************************************
* Player's main class & task
*/
@@ -42,7 +48,11 @@ class cspotPlayer : public bell::Task {
private:
std::string name;
bell::WrappedSemaphore clientConnected;
std::atomic<bool> isPaused, isConnected;
std::atomic<bool> isPaused;
enum states { ABORT, LINKED, DISCO };
std::atomic<states> state;
std::string credentials;
bool zeroConf;
int startOffset, volume = 0, bitrate = 160;
httpd_handle_t serverHandle;
@@ -57,6 +67,7 @@ private:
void eventHandler(std::unique_ptr<cspot::SpircHandler::Event> event);
void trackHandler(void);
size_t pcmWrite(uint8_t *pcm, size_t bytes, std::string_view trackId);
void enableZeroConf(void);
void runTask();
@@ -80,7 +91,24 @@ cspotPlayer::cspotPlayer(const char* name, httpd_handle_t server, int port, cspo
if ((item = cJSON_GetObjectItem(config, "bitrate")) != NULL) bitrate = item->valueint;
if ((item = cJSON_GetObjectItem(config, "deviceName") ) != NULL) this->name = item->valuestring;
else this->name = name;
cJSON_Delete(config);
if ((item = cJSON_GetObjectItem(config, "zeroConf")) != NULL) {
zeroConf = item->valueint;
cJSON_Delete(config);
} else {
zeroConf = true;
cJSON_AddNumberToObject(config, "zeroConf", 1);
config_set_cjson_str_and_free("cspot_config", config);
}
// get optional credentials from own NVS
if (!zeroConf) {
char *credentials = (char*) get_nvs_value_alloc_for_partition(NVS_DEFAULT_PART_NAME, spotify_ns.ns, NVS_TYPE_STR, spotify_ns.credentials, NULL);
if (credentials) {
this->credentials = credentials;
free(credentials);
}
}
if (bitrate != 96 && bitrate != 160 && bitrate != 320) bitrate = 160;
}
@@ -207,7 +235,7 @@ void cspotPlayer::eventHandler(std::unique_ptr<cspot::SpircHandler::Event> event
}
case cspot::SpircHandler::EventType::DISC:
cmdHandler(CSPOT_DISC);
isConnected = false;
state = DISCO;
break;
case cspot::SpircHandler::EventType::SEEK: {
cmdHandler(CSPOT_SEEK, std::get<int>(event->data));
@@ -265,7 +293,7 @@ void cspotPlayer::command(cspot_event_t event) {
* generate any cspot::event */
case CSPOT_DISC:
cmdHandler(CSPOT_DISC);
isConnected = false;
state = ABORT;
break;
// spirc->setRemoteVolume does not generate a cspot::event so call cmdHandler
case CSPOT_VOLUME_UP:
@@ -285,13 +313,13 @@ void cspotPlayer::command(cspot_event_t event) {
}
}
void cspotPlayer::runTask() {
void cspotPlayer::enableZeroConf(void) {
httpd_uri_t request = {
.uri = "/spotify_info",
.method = HTTP_GET,
.handler = ::handleGET,
.user_ctx = NULL,
};
};
// register GET and POST handler for built-in server
httpd_register_uri_handler(serverHandle, &request);
@@ -299,20 +327,34 @@ void cspotPlayer::runTask() {
request.handler = ::handlePOST;
httpd_register_uri_handler(serverHandle, &request);
// construct blob for that player
blob = std::make_unique<cspot::LoginBlob>(name);
CSPOT_LOG(info, "ZeroConf mode (port %d)", serverPort);
// Register mdns service, for spotify to find us
bell::MDNSService::registerService( blob->getDeviceName(), "_spotify-connect", "_tcp", "", serverPort,
{ {"VERSION", "1.0"}, {"CPath", "/spotify_info"}, {"Stack", "SP"} });
{ {"VERSION", "1.0"}, {"CPath", "/spotify_info"}, {"Stack", "SP"} });
}
void cspotPlayer::runTask() {
bool useZeroConf = zeroConf;
// construct blob for that player
blob = std::make_unique<cspot::LoginBlob>(name);
CSPOT_LOG(info, "CSpot instance service name %s (id %s)", blob->getDeviceName().c_str(), blob->getDeviceId().c_str());
if (!zeroConf && !credentials.empty()) {
blob->loadJson(credentials);
CSPOT_LOG(info, "Reusable credentials mode");
} else {
// whether we want it or not we must use ZeroConf
useZeroConf = true;
enableZeroConf();
}
// gone with the wind...
while (1) {
clientConnected.wait();
CSPOT_LOG(info, "Spotify client connected for %s", name.c_str());
if (useZeroConf) clientConnected.wait();
CSPOT_LOG(info, "Spotify client launched for %s", name.c_str());
auto ctx = cspot::Context::createFromBlob(blob);
@@ -321,12 +363,26 @@ void cspotPlayer::runTask() {
else ctx->config.audioFormat = AudioFormat_OGG_VORBIS_160;
ctx->session->connectWithRandomAp();
auto token = ctx->session->authenticate(blob);
ctx->config.authData = ctx->session->authenticate(blob);
// Auth successful
if (token.size() > 0) {
if (ctx->config.authData.size() > 0) {
// we might have been forced to use zeroConf, so store credentials and reset zeroConf usage
if (!zeroConf) {
useZeroConf = false;
// can't call store_nvs... from a task running on EXTRAM stack
TimerHandle_t timer = xTimerCreate( "credentials", 1, pdFALSE, strdup(ctx->getCredentialsJson().c_str()),
[](TimerHandle_t xTimer) {
auto credentials = (char*) pvTimerGetTimerID(xTimer);
store_nvs_value_len_for_partition(NVS_DEFAULT_PART_NAME, spotify_ns.ns, NVS_TYPE_STR, spotify_ns.credentials, credentials, 0);
free(credentials);
xTimerDelete(xTimer, portMAX_DELAY);
} );
xTimerStart(timer, portMAX_DELAY);
}
spirc = std::make_unique<cspot::SpircHandler>(ctx);
isConnected = true;
state = LINKED;
// set call back to calculate a hash on trackId
spirc->getTrackPlayer()->setDataCallback(
@@ -347,7 +403,7 @@ void cspotPlayer::runTask() {
cmdHandler(CSPOT_VOLUME, volume);
// exit when player has stopped (received a DISC)
while (isConnected) {
while (state == LINKED) {
ctx->session->handlePacket();
// low-accuracy polling events
@@ -371,23 +427,32 @@ void cspotPlayer::runTask() {
spirc->setPause(true);
}
}
// on disconnect, stay in the core loop unless we are in ZeroConf mode
if (state == DISCO) {
// update volume then
cJSON *config = config_alloc_get_cjson("cspot_config");
cJSON_DeleteItemFromObject(config, "volume");
cJSON_AddNumberToObject(config, "volume", volume);
config_set_cjson_str_and_free("cspot_config", config);
// in ZeroConf mod, stay connected (in this loop)
if (!zeroConf) state = LINKED;
}
}
spirc->disconnect();
spirc.reset();
CSPOT_LOG(info, "disconnecting player %s", name.c_str());
} else {
CSPOT_LOG(error, "failed authentication, forcing ZeroConf");
if (!useZeroConf) enableZeroConf();
useZeroConf = true;
}
// we want to release memory ASAP and for sure
ctx.reset();
token.clear();
// update volume when we disconnect
cJSON *config = config_alloc_get_cjson("cspot_config");
cJSON_DeleteItemFromObject(config, "volume");
cJSON_AddNumberToObject(config, "volume", volume);
config_set_cjson_str_and_free("cspot_config", config);
}
}

View File

@@ -1,7 +1,9 @@
file(GLOB AACDEC_SOURCES "src/*.c")
file(GLOB AACDEC_HEADERS "src/*.h" "oscl/*.h" "include/*.h")
add_library(opencore-aacdec SHARED ${AACDEC_SOURCES})
add_library(opencore-aacdec STATIC ${AACDEC_SOURCES})
if(NOT MSVC)
target_compile_options(opencore-aacdec PRIVATE -Wno-array-parameter)
endif()
add_definitions(-DAAC_PLUS -DHQ_SBR -DPARAMETRICSTEREO -DC_EQUIVALENT)
target_compile_options(opencore-aacdec PRIVATE -Wno-array-parameter)
target_include_directories(opencore-aacdec PUBLIC "src/" "oscl/" "include/")

View File

@@ -31,8 +31,8 @@ class CryptoMbedTLS {
CryptoMbedTLS();
~CryptoMbedTLS();
// Base64
std::vector<uint8_t> base64Decode(const std::string& data);
std::string base64Encode(const std::vector<uint8_t>& data);
static std::vector<uint8_t> base64Decode(const std::string& data);
static std::string base64Encode(const std::vector<uint8_t>& data);
// Sha1
void sha1Init();

View File

@@ -6,7 +6,16 @@
#include "LoginBlob.h"
#include "MercurySession.h"
#include "TimeProvider.h"
#include "Crypto.h"
#include "protobuf/metadata.pb.h"
#include "protobuf/authentication.pb.h" // for AuthenticationType_AUTHE...
#ifdef BELL_ONLY_CJSON
#include "cJSON.h"
#else
#include "nlohmann/detail/json_pointer.hpp" // for json_pointer<>::string_t
#include "nlohmann/json.hpp" // for basic_json<>::object_t, basic_json
#include "nlohmann/json_fwd.hpp" // for json
#endif
namespace cspot {
struct Context {
@@ -26,6 +35,28 @@ struct Context {
std::shared_ptr<TimeProvider> timeProvider;
std::shared_ptr<cspot::MercurySession> session;
std::string getCredentialsJson() {
#ifdef BELL_ONLY_CJSON
cJSON* json_obj = cJSON_CreateObject();
cJSON_AddStringToObject(json_obj, "authData", Crypto::base64Encode(config.authData).c_str());
cJSON_AddNumberToObject(json_obj, "authType", AuthenticationType_AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS);
cJSON_AddStringToObject(json_obj, "username", config.username.c_str());
char* str = cJSON_PrintUnformatted(json_obj);
cJSON_Delete(json_obj);
std::string json_objStr(str);
free(str);
return json_objStr;
#else
nlohmann::json obj;
obj["authData"] = Crypto::base64Encode(config.authData);
obj["authType"] = AuthenticationType_AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS;
obj["username"] = config.username;
return obj.dump();
#endif
}
static std::shared_ptr<Context> createFromBlob(
std::shared_ptr<LoginBlob> blob) {

View File

@@ -3,3 +3,8 @@ LoginCredentials.auth_data max_size:512, fixed_length:false
SystemInfo.system_information_string max_size:16, fixed_length:false
SystemInfo.device_id max_size:50, fixed_length:false
ClientResponseEncrypted.version_string max_size:32, fixed_length:false
APWelcome.canonical_username max_size:30, fixed_length:false
APWelcome.reusable_auth_credentials max_size:512, fixed_length:false
APWelcome.lfs_secret max_size:128, fixed_length:false
AccountInfoFacebook.access_token max_size:128, fixed_length:false
AccountInfoFacebook.machine_id max_size:50, fixed_length:false

View File

@@ -37,6 +37,11 @@ enum Os {
OS_BCO = 0x16;
}
enum AccountType {
Spotify = 0x0;
Facebook = 0x1;
}
enum AuthenticationType {
AUTHENTICATION_USER_PASS = 0x0;
AUTHENTICATION_STORED_SPOTIFY_CREDENTIALS = 0x1;
@@ -63,3 +68,27 @@ message ClientResponseEncrypted {
required SystemInfo system_info = 0x32;
optional string version_string = 0x46;
}
message APWelcome {
required string canonical_username = 0xa;
required AccountType account_type_logged_in = 0x14;
required AccountType credentials_type_logged_in = 0x19;
required AuthenticationType reusable_auth_credentials_type = 0x1e;
required bytes reusable_auth_credentials = 0x28;
optional bytes lfs_secret = 0x32;
optional AccountInfo account_info = 0x3c;
optional AccountInfoFacebook fb = 0x46;
}
message AccountInfo {
optional AccountInfoSpotify spotify = 0x1;
optional AccountInfoFacebook facebook = 0x2;
}
message AccountInfoSpotify {
}
message AccountInfoFacebook {
optional string access_token = 0x1;
optional string machine_id = 0x2;
}

View File

@@ -142,8 +142,8 @@ void LoginBlob::loadJson(const std::string& json) {
cJSON* root = cJSON_Parse(json.c_str());
this->authType = cJSON_GetObjectItem(root, "authType")->valueint;
this->username = cJSON_GetObjectItem(root, "username")->valuestring;
std::string authDataObject =
cJSON_GetObjectItem(root, "authData")->valuestring;
std::string authDataObject = cJSON_GetObjectItem(root, "authData")->valuestring;
this->authData = crypto->base64Decode(authDataObject);
cJSON_Delete(root);
#else
auto root = nlohmann::json::parse(json);

View File

@@ -17,6 +17,10 @@
#include "PlainConnection.h" // for PlainConnection, timeoutCallback
#include "ShannonConnection.h" // for ShannonConnection
#include "pb_decode.h"
#include "NanoPBHelper.h" // for pbPutString, pbEncode, pbDecode
#include "protobuf/authentication.pb.h"
using random_bytes_engine =
std::independent_bits_engine<std::default_random_engine, CHAR_BIT, uint8_t>;
@@ -79,9 +83,13 @@ std::vector<uint8_t> Session::authenticate(std::shared_ptr<LoginBlob> blob) {
auto packet = this->shanConn->recvPacket();
switch (packet.command) {
case AUTH_SUCCESSFUL_COMMAND: {
APWelcome welcome;
CSPOT_LOG(debug, "Authorization successful");
pbDecode(welcome, APWelcome_fields, packet.data);
return std::vector<uint8_t>(
{0x1}); // TODO: return actual reusable credentaials to be stored somewhere
welcome.reusable_auth_credentials.bytes,
welcome.reusable_auth_credentials.bytes + welcome.reusable_auth_credentials.size
);
break;
}
case AUTH_DECLINED_COMMAND: {