diff options
Diffstat (limited to 'src/main/java/kr/syeyoung/dungeonsguide/auth')
8 files changed, 604 insertions, 0 deletions
diff --git a/src/main/java/kr/syeyoung/dungeonsguide/auth/AuthManager.java b/src/main/java/kr/syeyoung/dungeonsguide/auth/AuthManager.java new file mode 100644 index 00000000..5588a4a2 --- /dev/null +++ b/src/main/java/kr/syeyoung/dungeonsguide/auth/AuthManager.java @@ -0,0 +1,150 @@ +package kr.syeyoung.dungeonsguide.auth; + +import com.google.common.base.Throwables; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.google.gson.JsonObject; +import com.mojang.authlib.exceptions.AuthenticationException; +import kr.syeyoung.dungeonsguide.auth.authprovider.AuthProvider; +import kr.syeyoung.dungeonsguide.auth.authprovider.DgAuth.DgAuth; +import kr.syeyoung.dungeonsguide.auth.authprovider.DgAuth.DgAuthUtil; +import kr.syeyoung.dungeonsguide.mod.chat.ChatTransmitter; +import kr.syeyoung.dungeonsguide.mod.events.impl.AuthChangedEvent; +import kr.syeyoung.dungeonsguide.mod.stomp.StompManager; +import lombok.Setter; +import net.minecraft.client.Minecraft; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.util.Objects; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + + +public class AuthManager { + Logger logger = LogManager.getLogger("AuthManger"); + + private static AuthManager instance; + + public static AuthManager getInstance() { + if(instance == null) instance = new AuthManager(); + return instance; + } + + + @Setter + private String baseserverurl = "https://dungeons.guide"; + + private AuthProvider currentProvider; + + public String getToken() { + if (currentProvider != null && currentProvider.getToken() != null) { + return currentProvider.getToken(); + } + return null; + } + + public KeyPair getKeyPair(){ + if (currentProvider != null && currentProvider.getToken() != null) { + return currentProvider.getRsaKey(); + } + return null; + } + + + boolean initlock = false; + + public void init() { + if (initlock) { + logger.info("Cannot init AuthManger twice"); + return; + } + + reauth(); + + initlock = true; + + + MinecraftForge.EVENT_BUS.register(this); + + ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("DgAuth Pool").build(); + final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1, namedThreadFactory); + scheduler.scheduleAtFixedRate(() -> { + if (getToken() != null) { + JsonObject obj = DgAuthUtil.getJwtPayload(getToken()); + if (!obj.get("uuid").getAsString().replace("-", "").equals(Minecraft.getMinecraft().getSession().getPlayerID())) { + shouldReAuth = true; + } + } + }, 10,2000, TimeUnit.MILLISECONDS); + } + + boolean shouldReAuth = true; + int tickCounter; + + @SubscribeEvent + public void onTickClientTick(TickEvent.ClientTickEvent event) { + if (event.phase != TickEvent.Phase.START) return; + + if (tickCounter % 200 == 0) { + tickCounter = 0; + reauth(); + } + tickCounter++; + + } + + public boolean isPlebUser(){ + return Objects.equals(getInstance().getPlanType(), "OPENSOURCE"); + } + + public String getPlanType(){ + if(getToken() == null) return null; + + + JsonObject jwt = DgAuthUtil.getJwtPayload(getToken()); + + if(!jwt.has("plan")) return null; + + return jwt.get("plan").getAsString(); + + } + + void reauth() { + if (!shouldReAuth) return; + + shouldReAuth = false; + + currentProvider = null; + try { + currentProvider = new DgAuth(baseserverurl).createAuthProvider(); + if (currentProvider.getToken() == null) { + shouldReAuth = true; + currentProvider = null; + ChatTransmitter.addToQueue("§eDungeons Guide §7:: §r§cDG auth failed, trying again in ten seconds", true); + logger.info("DG auth failed, trying again in a second"); + } else { + // RE-AUTHed SUCCESSFULLY HOORAY + // for some reason the forge events don't work in pre init, so I call the callback directly + StompManager.getInstance().init(); + MinecraftForge.EVENT_BUS.post(new AuthChangedEvent()); + } + } catch (NoSuchAlgorithmException | AuthenticationException | IOException e) { + + shouldReAuth = true; + currentProvider = null; + ChatTransmitter.addToQueue("§eDungeons Guide §7:: §r§cDG auth failed, trying again in ten seconds", true); + logger.error("Re-auth failed with message {}, trying again in a ten seconds", String.valueOf(Throwables.getRootCause(e))); + } + + } + + +} diff --git a/src/main/java/kr/syeyoung/dungeonsguide/auth/AuthUtil.java b/src/main/java/kr/syeyoung/dungeonsguide/auth/AuthUtil.java new file mode 100644 index 00000000..d96b054d --- /dev/null +++ b/src/main/java/kr/syeyoung/dungeonsguide/auth/AuthUtil.java @@ -0,0 +1,70 @@ +package kr.syeyoung.dungeonsguide.auth; + +import com.google.gson.JsonElement; +import com.google.gson.JsonParser; + +import javax.crypto.*; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.HttpsURLConnection; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; +import java.security.*; + +public class AuthUtil { + private AuthUtil() {} + + public static KeyPair getKeyPair() throws NoSuchAlgorithmException { + KeyPairGenerator a = null; + a = KeyPairGenerator.getInstance("RSA"); + a.initialize(1024); + return a.generateKeyPair(); + } + + + public static JsonElement getJsonSecured(String u) throws IOException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException{ + + int length = 0; + CipherInputStream cipherInputStream = null; + + 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", AuthManager.getInstance().getToken()); + httpsURLConnection.setDoInput(true); + httpsURLConnection.setDoOutput(true); + + InputStream inputStream = httpsURLConnection.getInputStream(); + byte[] lengthPayload = new byte[4]; + inputStream.read(lengthPayload); + 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, AuthManager.getInstance().getKeyPair().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 = new CipherInputStream(inputStream, cipher); + cipherInputStream.read(lengthPayload); + length = ((lengthPayload[0] & 0xFF) << 24) | + ((lengthPayload[1] & 0xFF) << 16) | + ((lengthPayload[2] & 0xFF) << 8) | + ((lengthPayload[3] & 0xFF)); + + httpsURLConnection.disconnect(); + + return new JsonParser().parse(new InputStreamReader(cipherInputStream)); + } +} diff --git a/src/main/java/kr/syeyoung/dungeonsguide/auth/InvalidDungeonsGuideCredentialsException.java b/src/main/java/kr/syeyoung/dungeonsguide/auth/InvalidDungeonsGuideCredentialsException.java new file mode 100644 index 00000000..98caa049 --- /dev/null +++ b/src/main/java/kr/syeyoung/dungeonsguide/auth/InvalidDungeonsGuideCredentialsException.java @@ -0,0 +1,8 @@ +package kr.syeyoung.dungeonsguide.auth; + +public class InvalidDungeonsGuideCredentialsException extends Throwable { + + public InvalidDungeonsGuideCredentialsException(String message) { + super(message); + } +} diff --git a/src/main/java/kr/syeyoung/dungeonsguide/auth/ResourceManager.java b/src/main/java/kr/syeyoung/dungeonsguide/auth/ResourceManager.java new file mode 100644 index 00000000..56d46aea --- /dev/null +++ b/src/main/java/kr/syeyoung/dungeonsguide/auth/ResourceManager.java @@ -0,0 +1,164 @@ +package kr.syeyoung.dungeonsguide.auth; + +import lombok.Setter; +import net.minecraftforge.common.MinecraftForge; +import org.apache.commons.codec.binary.Base64; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import javax.crypto.*; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.net.ssl.HttpsURLConnection; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.security.*; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.HashMap; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +public class ResourceManager { + + Logger logger = LogManager.getLogger("ResourceManager"); + + @Setter + private String baseUrl; + @Setter + private String BASE64_X509ENCODEDKEYSPEC; + private final HashMap<String, byte[]> loadedResources = new HashMap<>(); + + + private static ResourceManager instance; + public static ResourceManager getInstance() { + if(instance == null) { + instance = new ResourceManager(); + MinecraftForge.EVENT_BUS.register(instance); + } + return instance; + } + + private ResourceManager() { + } + + public Map<String, byte[]> getResources() { + return loadedResources; + } + + + public void downloadAssets(String version) throws InvalidDungeonsGuideCredentialsException { + if(AuthManager.getInstance().getToken() == null) throw new InvalidDungeonsGuideCredentialsException("Not Authenticated while downloading assets"); + try { + // version not being null indicates that the user is "premium" + // so we download the special version + if (version != null) + downloadSafe( baseUrl + "/resource/version?v=" + version, true); + + if(!AuthManager.getInstance().isPlebUser()){ + downloadSafe(baseUrl + "/resource/roomdata", false); + } else { + logger.error("The current User is a pleb not downloading user data"); + } + + } catch (Exception t) { + t.printStackTrace(); + } + + } + + private void downloadSafe(String url, boolean isValidateSignature) throws IOException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, InvalidAlgorithmParameterException, 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", AuthManager.getInstance().getToken()); + 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, AuthManager.getInstance().getKeyPair().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; + if (isValidateSignature) { + 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(getPublicKey(BASE64_X509ENCODEDKEYSPEC)); + 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; + while((p = zipInputStream.read(buffer)) > 0) { + byteArrayOutputStream.write(buffer, 0, p); + } + this.loadedResources.put(zipEntry.getName(), byteArrayOutputStream.toByteArray()); + } + } + + + public static PublicKey getPublicKey(String base64X509EncodedKeySpec) throws NoSuchAlgorithmException, InvalidKeySpecException { + X509EncodedKeySpec spec = new X509EncodedKeySpec(Base64.decodeBase64(base64X509EncodedKeySpec)); + + return KeyFactory.getInstance("RSA").generatePublic(spec); + } + +} diff --git a/src/main/java/kr/syeyoung/dungeonsguide/auth/authprovider/AuthProvider.java b/src/main/java/kr/syeyoung/dungeonsguide/auth/authprovider/AuthProvider.java new file mode 100644 index 00000000..ba42574e --- /dev/null +++ b/src/main/java/kr/syeyoung/dungeonsguide/auth/authprovider/AuthProvider.java @@ -0,0 +1,16 @@ +package kr.syeyoung.dungeonsguide.auth.authprovider; + +import com.mojang.authlib.exceptions.AuthenticationException; + +import java.io.IOException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; + +public interface AuthProvider { + String getToken(); + + KeyPair getRsaKey(); + + + AuthProvider createAuthProvider() throws NoSuchAlgorithmException, AuthenticationException, IOException; +} diff --git a/src/main/java/kr/syeyoung/dungeonsguide/auth/authprovider/DgAuth/DgAuth.java b/src/main/java/kr/syeyoung/dungeonsguide/auth/authprovider/DgAuth/DgAuth.java new file mode 100644 index 00000000..0e7a05f6 --- /dev/null +++ b/src/main/java/kr/syeyoung/dungeonsguide/auth/authprovider/DgAuth/DgAuth.java @@ -0,0 +1,46 @@ +package kr.syeyoung.dungeonsguide.auth.authprovider.DgAuth; + +import com.mojang.authlib.exceptions.AuthenticationException; +import kr.syeyoung.dungeonsguide.auth.AuthUtil; +import kr.syeyoung.dungeonsguide.auth.authprovider.AuthProvider; + +import java.io.IOException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; + +public class DgAuth implements AuthProvider { + + private final String authServerUrl; + + public DgAuth(String authServerUrl){ + this.authServerUrl = authServerUrl; + } + + private String token; + private KeyPair rsaKey; + + @Override + public String getToken() { + return token; + } + + @Override + public KeyPair getRsaKey() { + return rsaKey; + } + + + @Override + public AuthProvider createAuthProvider() throws NoSuchAlgorithmException, AuthenticationException, IOException { + this.rsaKey = AuthUtil.getKeyPair(); + + String tempToken = DgAuthUtil.requestAuth(this.authServerUrl); + + DgAuthUtil.checkSessionAuthenticity(tempToken); + + this.token = DgAuthUtil.verifyAuth(tempToken, rsaKey.getPublic(), authServerUrl); + + return this; + } + +} diff --git a/src/main/java/kr/syeyoung/dungeonsguide/auth/authprovider/DgAuth/DgAuthUtil.java b/src/main/java/kr/syeyoung/dungeonsguide/auth/authprovider/DgAuth/DgAuthUtil.java new file mode 100644 index 00000000..b01fba50 --- /dev/null +++ b/src/main/java/kr/syeyoung/dungeonsguide/auth/authprovider/DgAuth/DgAuthUtil.java @@ -0,0 +1,88 @@ +package kr.syeyoung.dungeonsguide.auth.authprovider.DgAuth; + +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 org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.IOUtils; + +import javax.net.ssl.HttpsURLConnection; +import java.io.IOException; +import java.math.BigInteger; +import java.net.URL; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; + +public class DgAuthUtil { + private DgAuthUtil(){} + + public static String requestAuth(String baseurl) throws IOException { + GameProfile profile = Minecraft.getMinecraft().getSession().getProfile(); + + HttpsURLConnection connection = (HttpsURLConnection) new URL(baseurl + "/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())); + + JsonObject json = (JsonObject) new JsonParser().parse(payload); + + if (!"ok".equals(json.get("status").getAsString())) { + return null; + } + return json.get("data").getAsString(); + } + + public static void checkSessionAuthenticity(String tempToken) throws NoSuchAlgorithmException, AuthenticationException { + JsonObject d = getJwtPayload(tempToken); + byte[] sharedSecret = Base64.decodeBase64(d.get("sharedSecret").getAsString()); + byte[] publicKey =Base64.decodeBase64(d.get("publicKey").getAsString()); + String hash = calculateServerHash(sharedSecret, publicKey); + + Session session = Minecraft.getMinecraft().getSession(); + MinecraftSessionService yggdrasilMinecraftSessionService = Minecraft.getMinecraft().getSessionService(); + yggdrasilMinecraftSessionService.joinServer(session.getProfile(), session.getToken(), hash); + } + + public static String verifyAuth(String tempToken, PublicKey clientKey, String baseurl) throws IOException { + HttpsURLConnection urlConnection = (HttpsURLConnection) new URL(baseurl + "/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())); + + JsonObject jsonObject = (JsonObject) new JsonParser().parse(payload); + if (!"ok".equals(jsonObject.get("status").getAsString())) { + return null; + } + return jsonObject.get("data").getAsString(); + } + + public static 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); + } + + public static 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); + } +} diff --git a/src/main/java/kr/syeyoung/dungeonsguide/auth/authprovider/NullAuth.java b/src/main/java/kr/syeyoung/dungeonsguide/auth/authprovider/NullAuth.java new file mode 100644 index 00000000..ec7e9aed --- /dev/null +++ b/src/main/java/kr/syeyoung/dungeonsguide/auth/authprovider/NullAuth.java @@ -0,0 +1,62 @@ +package kr.syeyoung.dungeonsguide.auth.authprovider; + +import com.mojang.authlib.exceptions.AuthenticationException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; + +public class NullAuth implements AuthProvider { + + Logger logger = LogManager.getLogger("NullAuth"); + + @Override + public String getToken() { + return "TOKEN"; + } + + @Override + public KeyPair getRsaKey() { + return new KeyPair(new PublicKey() { + @Override + public String getAlgorithm() { + return null; + } + + @Override + public String getFormat() { + return null; + } + + @Override + public byte[] getEncoded() { + return new byte[0]; + } + }, new PrivateKey() { + @Override + public String getAlgorithm() { + return null; + } + + @Override + public String getFormat() { + return null; + } + + @Override + public byte[] getEncoded() { + return new byte[0]; + } + }); + } + + @Override + public AuthProvider createAuthProvider() throws NoSuchAlgorithmException, AuthenticationException, IOException { + return new NullAuth(); + } + +} |