// SPDX-License-Identifier: GPL-3.0-only /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * This file incorporates work covered by the following copyright and * permission notice: * * Copyright 2013-2021 MultiMC Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "AccountData.h" #include #include #include #include #include #include namespace { void tokenToJSONV3(QJsonObject &parent, Katabasis::Token t, const char * tokenName) { if(!t.persistent) { return; } QJsonObject out; if(t.issueInstant.isValid()) { out["iat"] = QJsonValue(t.issueInstant.toMSecsSinceEpoch() / 1000); } if(t.notAfter.isValid()) { out["exp"] = QJsonValue(t.notAfter.toMSecsSinceEpoch() / 1000); } bool save = false; if(!t.token.isEmpty()) { out["token"] = QJsonValue(t.token); save = true; } if(!t.refresh_token.isEmpty()) { out["refresh_token"] = QJsonValue(t.refresh_token); save = true; } if(t.extra.size()) { out["extra"] = QJsonObject::fromVariantMap(t.extra); save = true; } if(save) { parent[tokenName] = out; } } Katabasis::Token tokenFromJSONV3(const QJsonObject &parent, const char * tokenName) { Katabasis::Token out; auto tokenObject = parent.value(tokenName).toObject(); if(tokenObject.isEmpty()) { return out; } auto issueInstant = tokenObject.value("iat"); if(issueInstant.isDouble()) { out.issueInstant = QDateTime::fromMSecsSinceEpoch(((int64_t) issueInstant.toDouble()) * 1000); } auto notAfter = tokenObject.value("exp"); if(notAfter.isDouble()) { out.notAfter = QDateTime::fromMSecsSinceEpoch(((int64_t) notAfter.toDouble()) * 1000); } auto token = tokenObject.value("token"); if(token.isString()) { out.token = token.toString(); out.validity = Katabasis::Validity::Assumed; } auto refresh_token = tokenObject.value("refresh_token"); if(refresh_token.isString()) { out.refresh_token = refresh_token.toString(); } auto extra = tokenObject.value("extra"); if(extra.isObject()) { out.extra = extra.toObject().toVariantMap(); } return out; } void profileToJSONV3(QJsonObject &parent, MinecraftProfile p, const char * tokenName) { if(p.id.isEmpty()) { return; } QJsonObject out; out["id"] = QJsonValue(p.id); out["name"] = QJsonValue(p.name); if(!p.currentCape.isEmpty()) { out["cape"] = p.currentCape; } { QJsonObject skinObj; skinObj["id"] = p.skin.id; skinObj["url"] = p.skin.url; skinObj["variant"] = p.skin.variant; if(p.skin.data.size()) { skinObj["data"] = QString::fromLatin1(p.skin.data.toBase64()); } out["skin"] = skinObj; } QJsonArray capesArray; for(auto & cape: p.capes) { QJsonObject capeObj; capeObj["id"] = cape.id; capeObj["url"] = cape.url; capeObj["alias"] = cape.alias; if(cape.data.size()) { capeObj["data"] = QString::fromLatin1(cape.data.toBase64()); } capesArray.push_back(capeObj); } out["capes"] = capesArray; parent[tokenName] = out; } MinecraftProfile profileFromJSONV3(const QJsonObject &parent, const char * tokenName) { MinecraftProfile out; auto tokenObject = parent.value(tokenName).toObject(); if(tokenObject.isEmpty()) { return out; } { auto idV = tokenObject.value("id"); auto nameV = tokenObject.value("name"); if(!idV.isString() || !nameV.isString()) { qWarning() << "mandatory profile attributes are missing or of unexpected type"; return MinecraftProfile(); } out.name = nameV.toString(); out.id = idV.toString(); } { auto skinV = tokenObject.value("skin"); if(!skinV.isObject()) { qWarning() << "skin is missing"; return MinecraftProfile(); } auto skinObj = skinV.toObject(); auto idV = skinObj.value("id"); auto urlV = skinObj.value("url"); auto variantV = skinObj.value("variant"); if(!idV.isString() || !urlV.isString() || !variantV.isString()) { qWarning() << "mandatory skin attributes are missing or of unexpected type"; return MinecraftProfile(); } out.skin.id = idV.toString(); out.skin.url = urlV.toString(); out.skin.variant = variantV.toString(); // data for skin is optional auto dataV = skinObj.value("data"); if(dataV.isString()) { // TODO: validate base64 out.skin.data = QByteArray::fromBase64(dataV.toString().toLatin1()); } else if (!dataV.isUndefined()) { qWarning() << "skin data is something unexpected"; return MinecraftProfile(); } } { auto capesV = tokenObject.value("capes"); if(!capesV.isArray()) { qWarning() << "capes is not an array!"; return MinecraftProfile(); } auto capesArray = capesV.toArray(); for(auto capeV: capesArray) { if(!capeV.isObject()) { qWarning() << "cape is not an object!"; return MinecraftProfile(); } auto capeObj = capeV.toObject(); auto idV = capeObj.value("id"); auto urlV = capeObj.value("url"); auto aliasV = capeObj.value("alias"); if(!idV.isString() || !urlV.isString() || !aliasV.isString()) { qWarning() << "mandatory skin attributes are missing or of unexpected type"; return MinecraftProfile(); } Cape cape; cape.id = idV.toString(); cape.url = urlV.toString(); cape.alias = aliasV.toString(); // data for cape is optional. auto dataV = capeObj.value("data"); if(dataV.isString()) { // TODO: validate base64 cape.data = QByteArray::fromBase64(dataV.toString().toLatin1()); } else if (!dataV.isUndefined()) { qWarning() << "cape data is something unexpected"; return MinecraftProfile(); } out.capes[cape.id] = cape; } } // current cape { auto capeV = tokenObject.value("cape"); if(capeV.isString()) { auto currentCape = capeV.toString(); if(out.capes.contains(currentCape)) { out.currentCape = currentCape; } } } out.validity = Katabasis::Validity::Assumed; return out; } void entitlementToJSONV3(QJsonObject &parent, MinecraftEntitlement p) { if(p.validity == Katabasis::Validity::None) { return; } QJsonObject out; out["ownsMinecraft"] = QJsonValue(p.ownsMinecraft); out["canPlayMinecraft"] = QJsonValue(p.canPlayMinecraft); parent["entitlement"] = out; } bool entitlementFromJSONV3(const QJsonObject &parent, MinecraftEntitlement & out) { auto entitlementObject = parent.value("entitlement").toObject(); if(entitlementObject.isEmpty()) { return false; } { auto ownsMinecraftV = entitlementObject.value("ownsMinecraft"); auto canPlayMinecraftV = entitlementObject.value("canPlayMinecraft"); if(!ownsMinecraftV.isBool() || !canPlayMinecraftV.isBool()) { qWarning() << "mandatory attributes are missing or of unexpected type"; return false; } out.canPlayMinecraft = canPlayMinecraftV.toBool(false); out.ownsMinecraft = ownsMinecraftV.toBool(false); out.validity = Katabasis::Validity::Assumed; } return true; } } bool AccountData::resumeStateFromV2(QJsonObject data) { // The JSON object must at least have a username for it to be valid. if (!data.value("username").isString()) { qCritical() << "Can't load Mojang account info from JSON object. Username field is missing or of the wrong type."; return false; } QString userName = data.value("username").toString(""); QString clientToken = data.value("clientToken").toString(""); QString accessToken = data.value("accessToken").toString(""); QJsonArray profileArray = data.value("profiles").toArray(); if (profileArray.size() < 1) { qCritical() << "Can't load Mojang account with username \"" << userName << "\". No profiles found."; return false; } struct AccountProfile { QString id; QString name; bool legacy; }; QList profiles; int currentProfileIndex = 0; int index = -1; QString currentProfile = data.value("activeProfile").toString(""); for (QJsonValue profileVal : profileArray) { index++; QJsonObject profileObject = profileVal.toObject(); QString id = profileObject.value("id").toString(""); QString name = profileObject.value("name").toString(""); bool legacy = profileObject.value("legacy").toBool(false); if (id.isEmpty() || name.isEmpty()) { qWarning() << "Unable to load a profile" << name << "because it was missing an ID or a name."; continue; } if(id == currentProfile) { currentProfileIndex = index; } profiles.append({id, name, legacy}); } auto & profile = profiles[currentProfileIndex]; type = AccountType::Mojang; legacy = profile.legacy; minecraftProfile.id = profile.id; minecraftProfile.name = profile.name; minecraftProfile.validity = Katabasis::Validity::Assumed; yggdrasilToken.token = accessToken; yggdrasilToken.extra["clientToken"] = clientToken; yggdrasilToken.extra["userName"] = userName; yggdrasilToken.validity = Katabasis::Validity::Assumed; validity_ = minecraftProfile.validity; return true; } bool AccountData::resumeStateFromV3(QJsonObject data) { auto typeV = data.value("type"); if(!typeV.isString()) { qWarning() << "Failed to parse account data: type is missing."; return false; } auto typeS = typeV.toString(); if(typeS == "MSA") { type = AccountType::MSA; } else if (typeS == "Mojang") { type = AccountType::Mojang; } else if (typeS == "Offline") { type = AccountType::Offline; } else { qWarning() << "Failed to parse account data: type is not recognized."; return false; } if(type == AccountType::Mojang) { legacy = data.value("legacy").toBool(false); canMigrateToMSA = data.value("canMigrateToMSA").toBool(false); } if(type == AccountType::MSA) { auto clientIDV = data.value("msa-client-id"); if (clientIDV.isString()) { msaClientID = clientIDV.toString(); } // leave msaClientID empty if it doesn't exist or isn't a string msaToken = tokenFromJSONV3(data, "msa"); userToken = tokenFromJSONV3(data, "utoken"); xboxApiToken = tokenFromJSONV3(data, "xrp-main"); mojangservicesToken = tokenFromJSONV3(data, "xrp-mc"); } yggdrasilToken = tokenFromJSONV3(data, "ygg"); minecraftProfile = profileFromJSONV3(data, "profile"); if(!entitlementFromJSONV3(data, minecraftEntitlement)) { if(minecraftProfile.validity != Katabasis::Validity::None) { minecraftEntitlement.canPlayMinecraft = true; minecraftEntitlement.ownsMinecraft = true; minecraftEntitlement.validity = Katabasis::Validity::Assumed; } } validity_ = minecraftProfile.validity; return true; } QJsonObject AccountData::saveState() const { QJsonObject output; if(type == AccountType::Mojang) { output["type"] = "Mojang"; if(legacy) { output["legacy"] = true; } if(canMigrateToMSA) { output["canMigrateToMSA"] = true; } } else if (type == AccountType::MSA) { output["type"] = "MSA"; output["msa-client-id"] = msaClientID; tokenToJSONV3(output, msaToken, "msa"); tokenToJSONV3(output, userToken, "utoken"); tokenToJSONV3(output, xboxApiToken, "xrp-main"); tokenToJSONV3(output, mojangservicesToken, "xrp-mc"); } else if (type == AccountType::Offline) { output["type"] = "Offline"; } tokenToJSONV3(output, yggdrasilToken, "ygg"); profileToJSONV3(output, minecraftProfile, "profile"); entitlementToJSONV3(output, minecraftEntitlement); return output; } QString AccountData::userName() const { if(type == AccountType::MSA) { return QString(); } return yggdrasilToken.extra["userName"].toString(); } QString AccountData::accessToken() const { return yggdrasilToken.token; } QString AccountData::clientToken() const { if(type != AccountType::Mojang) { return QString(); } return yggdrasilToken.extra["clientToken"].toString(); } void AccountData::setClientToken(QString clientToken) { if(type != AccountType::Mojang) { return; } yggdrasilToken.extra["clientToken"] = clientToken; } void AccountData::generateClientTokenIfMissing() { if(yggdrasilToken.extra.contains("clientToken")) { return; } invalidateClientToken(); } void AccountData::invalidateClientToken() { if(type != AccountType::Mojang) { return; } yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{-}]")); } QString AccountData::profileId() const { return minecraftProfile.id; } QString AccountData::profileName() const { if(minecraftProfile.name.size() == 0) { return QObject::tr("No profile (%1)").arg(accountDisplayString()); } else { return minecraftProfile.name; } } QString AccountData::accountDisplayString() const { switch(type) { case AccountType::Mojang: { return userName(); } case AccountType::Offline: { return QObject::tr(""); } case AccountType::MSA: { if(xboxApiToken.extra.contains("gtg")) { return xboxApiToken.extra["gtg"].toString(); } return "Xbox profile missing"; } default: { return "Invalid Account"; } } } QString AccountData::lastError() const { return errorString; }