/* * Dungeons Guide - The most intelligent Hypixel Skyblock Dungeons Mod * Copyright (C) 2021 cyoung06 * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ package kr.syeyoung.dungeonsguide; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.mojang.authlib.GameProfile; import com.mojang.authlib.exceptions.AuthenticationException; import com.mojang.authlib.minecraft.MinecraftSessionService; import net.minecraft.client.Minecraft; import net.minecraft.util.Session; import net.minecraftforge.fml.common.ProgressManager; import org.apache.commons.codec.binary.Base64; import org.apache.commons.io.IOUtils; import javax.crypto.*; import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import javax.net.ssl.*; import java.io.*; import java.math.BigInteger; import java.net.*; import java.security.*; import java.security.cert.CertificateException; import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; import java.util.HashMap; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; public class Authenticator { private KeyPair rsaKey; private String token; private final ProgressManager.ProgressBar progressBar; public String getToken() { return token; } private KeyPair getKeyPair() { KeyPairGenerator a = null; try { a = KeyPairGenerator.getInstance("RSA"); } catch (NoSuchAlgorithmException b) { } a.initialize(1024); this.rsaKey = a.generateKeyPair(); return this.rsaKey; } private PublicKey dgPublicKey; private PublicKey getDGPublicKey() throws NoSuchAlgorithmException, InvalidKeySpecException { if (dgPublicKey != null) return dgPublicKey; X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.decodeBase64("MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxO89qtwG67jNucQ9Y44c" + "IUs/B+5BeJPs7G+RG2gfs4/2+tzF/c1FLDc33M7yKw8aKk99vsBUY9Oo8gxxiEPB" + "JitP/qfon2THp94oM77ZTpHlmFoqbZMcKGZVI8yfvEL4laTM8Hw+qh5poQwtpEbK" + "Xo47AkxygxJasUnykER2+aSTZ6kWU2D4xiNtFA6lzqN+/oA+NaYfPS0amAvyVlHR" + "n/8IuGkxb5RrlqVssQstFnxsJuv88qdGSEqlcKq2tLeg9hb8eCnl2OFzvXmgbVER" + "0JaV+4Z02fVG1IlR3Xo1mSit7yIU6++3usRCjx2yfXpnGGJUW5pe6YETjNew3ax+" + "FAZ4GePWCdmS7FvBnbbABKo5pE06ZTfDUTCjQlAJQiUgoF6ntMJvQAXPu48Vr8q/" + "mTcuZWVnI6CDgyE7nNq3WNoq3397sBzxRohMxuqzl3T19zkfPKF05iV2Ju1HQMW5" + "I119bYrmVD240aGESZc20Sx/9g1BFpNzQbM5PGUlWJ0dhLjl2ge4ip2hHciY3OEY" + "p2Qy2k+xEdenpKdL+WMRimCQoO9gWe2Tp4NmP5dppDXZgPjXqjZpnGs0Uxs+fXqW" + "cwlg3MbX3rFl9so/fhVf4p9oXZK3ve7z5D6XSSDRYECvsKIa08WAxJ/U6n204E/4" + "xUF+3ZgFPdzZGn2PU7SsnOsCAwEAAQ==")); return dgPublicKey = KeyFactory.getInstance("RSA").generatePublic(spec); } public Authenticator(ProgressManager.ProgressBar progressBar) { this.progressBar = progressBar; progressBar.step("Generating KeyPair"); getKeyPair(); } public String authenticateAndDownload(String version) throws IOException, AuthenticationException, NoSuchAlgorithmException, CertificateException, KeyStoreException, KeyManagementException, InvalidKeySpecException, SignatureException { Session session = Minecraft.getMinecraft().getSession(); String sessionToken = session.getToken(); progressBar.step("Authenticating (1/2)"); String tempToken = requestAuth(session.getProfile()); MinecraftSessionService yggdrasilMinecraftSessionService = Minecraft.getMinecraft().getSessionService(); JsonObject d = getJwtPayload(tempToken); String hash = calculateServerHash(Base64.decodeBase64(d.get("sharedSecret").getAsString()), Base64.decodeBase64(d.get("publicKey").getAsString())); yggdrasilMinecraftSessionService.joinServer(session.getProfile(), sessionToken, hash); progressBar.step("Authenticating (2/2)"); this.token = verifyAuth(tempToken, this.rsaKey.getPublic()); try { progressBar.step("Downloading Jar"); if (version != null) downloadSafe(this.token, "https://dungeons.guide/resource/version?v=" + version, true); progressBar.step("Downloading Rooms"); downloadSafe(this.token, "https://dungeons.guide/resource/roomdata", false); } catch (Throwable t) { t.printStackTrace(); } return this.token; } public JsonObject getJwtPayload(String jwt) { String midPart = jwt.split("\\.")[1].replace("+", "-").replace("/", "_"); String base64Decode = new String(Base64.decodeBase64(midPart)); // padding return (JsonObject) new JsonParser().parse(base64Decode); } private String requestAuth(GameProfile profile) throws IOException, NoSuchAlgorithmException, CertificateException, KeyStoreException, KeyManagementException { HttpsURLConnection connection = (HttpsURLConnection) new URL("https://dungeons.guide/auth/requestAuth").openConnection(); connection.setRequestProperty("User-Agent", "DungeonsGuide/1.0"); connection.setRequestProperty("Content-Type", "application/json"); connection.setRequestMethod("POST"); connection.setDoInput(true); connection.setDoOutput(true); connection.getOutputStream().write(("{\"uuid\":\""+profile.getId().toString()+"\",\"nickname\":\""+profile.getName()+"\"}").getBytes()); String payload = String.join("\n", IOUtils.readLines(connection.getErrorStream() == null ? connection.getInputStream() : connection.getErrorStream())); if (connection.getResponseCode() >= 400) System.out.println("https://dungeons.guide/auth/requestAuth :: Received "+connection.getResponseCode()+" along with\n"+payload); JsonObject json = (JsonObject) new JsonParser().parse(payload); if (!"ok".equals(json.get("status").getAsString())) { return null; } return json.get("data").getAsString(); } private String verifyAuth(String tempToken, PublicKey clientKey) throws IOException { HttpsURLConnection urlConnection = (HttpsURLConnection) new URL("https://dungeons.guide/auth/authenticate").openConnection(); urlConnection.setRequestMethod("POST"); urlConnection.setRequestProperty("User-Agent", "DungeonsGuide/1.0"); urlConnection.setRequestProperty("Content-Type", "application/json"); urlConnection.setDoInput(true); urlConnection.setDoOutput(true); urlConnection.getOutputStream().write(("{\"jwt\":\""+tempToken+"\",\"publicKey\":\""+Base64.encodeBase64URLSafeString(clientKey.getEncoded())+"\"}").getBytes()); String payload = String.join("\n", IOUtils.readLines(urlConnection.getErrorStream() == null ? urlConnection.getInputStream() : urlConnection.getErrorStream())); if (urlConnection.getResponseCode() >= 400) System.out.println("https://dungeons.guide/auth/authenticate :: Received "+urlConnection.getResponseCode()+" along with\n"+payload); JsonObject jsonObject = (JsonObject) new JsonParser().parse(payload); if (!"ok".equals(jsonObject.get("status").getAsString())) { return null; } return jsonObject.get("data").getAsString(); } private final HashMap loadedResources = new HashMap(); public HashMap getResources() { return loadedResources; } private void downloadSafe(String dgToken, String url, boolean isValidateSignature) throws IOException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException, CertificateException, KeyStoreException, KeyManagementException, SignatureException, InvalidKeySpecException { HttpsURLConnection dgConnection = (HttpsURLConnection) new URL(url).openConnection(); dgConnection.setRequestProperty("User-Agent", "DungeonsGuide/1.0"); dgConnection.setRequestProperty("Content-Type", "application/json"); dgConnection.setRequestMethod("GET"); dgConnection.setRequestProperty("Authorization", dgToken); dgConnection.setDoInput(true); dgConnection.setDoOutput(true); InputStream inputStream = dgConnection.getInputStream(); byte[] lengthBytes = new byte[4]; inputStream.read(lengthBytes); int length = ((lengthBytes[0] & 0xFF) << 24) | ((lengthBytes[1] & 0xFF) << 16) | ((lengthBytes[2] & 0xFF) << 8) | ((lengthBytes[3] & 0xFF)); while (inputStream.available() < length) ; byte[] keyPayload = new byte[length]; inputStream.read(keyPayload); Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.DECRYPT_MODE, this.rsaKey.getPrivate()); byte[] h = cipher.doFinal(keyPayload); cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKeySpec keySpec = new SecretKeySpec(h, "AES"); IvParameterSpec ivSpec = new IvParameterSpec(h); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher); cipherInputStream.read(lengthBytes); length = ((lengthBytes[0] & 0xFF) << 24) | ((lengthBytes[1] & 0xFF) << 16) | ((lengthBytes[2] & 0xFF) << 8) | ((lengthBytes[3] & 0xFF)); int totalLen = length; ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buff = new byte[256]; while (totalLen > 0) { int len = cipherInputStream.read(buff, 0, Math.min(buff.length, totalLen)); totalLen -= len; bos.write(buff, 0, len); } byte[] body = bos.toByteArray(); byte[] signed = null; if (isValidateSignature) { progressBar.step("Validating Signature"); cipherInputStream.read(lengthBytes,0 , 4); length = ((lengthBytes[0] & 0xFF) << 24) | ((lengthBytes[1] & 0xFF) << 16) | ((lengthBytes[2] & 0xFF) << 8) | ((lengthBytes[3] & 0xFF)); totalLen = length; bos = new ByteArrayOutputStream(); while (totalLen > 0) { int len = cipherInputStream.read(buff, 0, Math.min(buff.length, totalLen)); totalLen -= len; bos.write(buff, 0, len); } signed = bos.toByteArray(); Signature sign = Signature.getInstance("SHA512withRSA"); sign.initVerify(getDGPublicKey()); sign.update(body); boolean truth = sign.verify(signed); if (!truth) throw new SignatureException("DG SIGNATURE FORGED"); } ZipInputStream zipInputStream = new ZipInputStream(new ByteArrayInputStream(body)); ZipEntry zipEntry; while ((zipEntry=zipInputStream.getNextEntry()) != null) { byte[] buffer = new byte[256]; ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); int p = 0; while((p = zipInputStream.read(buffer)) > 0) { byteArrayOutputStream.write(buffer, 0, p); } this.loadedResources.put(zipEntry.getName(), byteArrayOutputStream.toByteArray()); } dgConnection.disconnect(); } public JsonElement getJsonSecured(String u) throws IOException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException, CertificateException, KeyStoreException, KeyManagementException { HttpsURLConnection httpsURLConnection = (HttpsURLConnection) new URL(u).openConnection(); httpsURLConnection.setRequestProperty("User-Agent", "DungeonsGuide/1.0"); httpsURLConnection.setRequestProperty("Content-Type", "application/json"); httpsURLConnection.setRequestMethod("GET"); httpsURLConnection.setRequestProperty("Authorization", this.token); httpsURLConnection.setDoInput(true); httpsURLConnection.setDoOutput(true); InputStream inputStream = httpsURLConnection.getInputStream(); byte[] lengthPayload = new byte[4]; inputStream.read(lengthPayload); int length = ((lengthPayload[0] & 0xFF) << 24) | ((lengthPayload[1] & 0xFF) << 16) | ((lengthPayload[2] & 0xFF) << 8) | ((lengthPayload[3] & 0xFF)); while (inputStream.available() < length) ; byte[] keyPayload = new byte[length]; inputStream.read(keyPayload); Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); cipher.init(Cipher.DECRYPT_MODE, this.rsaKey.getPrivate()); byte[] AESKey = cipher.doFinal(keyPayload); cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKeySpec secretKeySpec = new SecretKeySpec(AESKey, "AES"); IvParameterSpec ivParameterSpec = new IvParameterSpec(AESKey); cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher); cipherInputStream.read(lengthPayload); length = ((lengthPayload[0] & 0xFF) << 24) | ((lengthPayload[1] & 0xFF) << 16) | ((lengthPayload[2] & 0xFF) << 8) | ((lengthPayload[3] & 0xFF)); JsonElement l = new JsonParser().parse(new InputStreamReader(cipherInputStream)); httpsURLConnection.disconnect(); return l; } public String calculateServerHash(byte[] a, byte[] b) throws NoSuchAlgorithmException { MessageDigest c = MessageDigest.getInstance("SHA-1"); c.update("".getBytes()); c.update(a); c.update(b); byte[] d = c.digest(); return new BigInteger(d).toString(16); } }