wip online mode

This commit is contained in:
creeper123123321 2020-10-25 17:44:03 -03:00
parent f207ac2365
commit fee83d594b
6 changed files with 287 additions and 53 deletions

View File

@ -18,6 +18,7 @@ import us.myles.ViaVersion.util.PipelineUtil
import java.util.concurrent.TimeUnit
import java.util.zip.Deflater
import java.util.zip.Inflater
import javax.crypto.Cipher
object ChannelInit : ChannelInitializer<Channel>() {
@ -25,6 +26,8 @@ object ChannelInit : ChannelInitializer<Channel>() {
val user = UserConnection(ch)
CloudPipeline(user)
ch.pipeline().addLast("timeout", ReadTimeoutHandler(30, TimeUnit.SECONDS))
.addLast("encrypt", CloudEncryptor())
.addLast("decrypt", CloudDecryptor())
.addLast("frame-encoder", FrameEncoder)
.addLast("frame-decoder", FrameDecoder())
.addLast("compress", CloudCompressor())
@ -36,9 +39,33 @@ object ChannelInit : ChannelInitializer<Channel>() {
}
}
class CloudDecryptor(var cipher: Cipher? = null) : MessageToMessageDecoder<ByteBuf>() {
override fun decode(ctx: ChannelHandlerContext, msg: ByteBuf, out: MutableList<Any>) {
val i = msg.readerIndex()
val size = msg.readableBytes()
if (cipher != null) {
msg.writerIndex(i + cipher!!.update(msg.nioBuffer(), msg.nioBuffer(i, cipher!!.getOutputSize(size))))
}
out.add(msg.retain())
}
}
class CloudEncryptor(var cipher: Cipher? = null) : MessageToMessageEncoder<ByteBuf>() {
override fun encode(ctx: ChannelHandlerContext?, msg: ByteBuf, out: MutableList<Any>) {
val i = msg.readerIndex()
val size = msg.readableBytes()
if (cipher != null) {
msg.writerIndex(i + cipher!!.update(msg.nioBuffer(), msg.nioBuffer(i, cipher!!.getOutputSize(size))))
}
out.add(msg.retain())
}
}
class BackendInit(val user: UserConnection) : ChannelInitializer<Channel>() {
override fun initChannel(ch: Channel) {
ch.pipeline().addLast("timeout", ReadTimeoutHandler(30, TimeUnit.SECONDS))
.addLast("encrypt", CloudEncryptor())
.addLast("decrypt", CloudDecryptor())
.addLast("frame-encoder", FrameEncoder)
.addLast("frame-decoder", FrameDecoder())
.addLast("compress", CloudCompressor())

View File

@ -4,7 +4,6 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder
import de.gerrygames.viarewind.api.ViaRewindPlatform
import io.netty.buffer.ByteBuf
import io.netty.channel.DefaultEventLoop
import io.netty.channel.socket.SocketChannel
import nl.matsv.viabackwards.api.ViaBackwardsPlatform
import us.myles.ViaVersion.AbstractViaConfig
import us.myles.ViaVersion.api.Via
@ -15,7 +14,6 @@ import us.myles.ViaVersion.api.boss.BossColor
import us.myles.ViaVersion.api.boss.BossStyle
import us.myles.ViaVersion.api.command.ViaCommandSender
import us.myles.ViaVersion.api.configuration.ConfigurationProvider
import us.myles.ViaVersion.api.data.StoredObject
import us.myles.ViaVersion.api.data.UserConnection
import us.myles.ViaVersion.api.platform.*
import us.myles.ViaVersion.api.protocol.ProtocolRegistry
@ -186,11 +184,4 @@ object CloudVersionProvider : VersionProvider() {
if (ver != null) return ver
return super.getServerProtocol(connection)
}
}
data class CloudData(val userConnection: UserConnection,
var backendVer: Int,
var backendChannel: SocketChannel? = null,
var frontOnline: Boolean,
var pendingStatus: Boolean = false
) : StoredObject(userConnection)
}

View File

@ -1,13 +1,19 @@
package com.github.creeper123123321.viaaas
import com.google.common.net.UrlEscapers
import com.google.gson.JsonObject
import io.ktor.client.request.*
import io.ktor.http.cio.websocket.*
import io.netty.bootstrap.Bootstrap
import io.netty.buffer.ByteBufAllocator
import io.netty.channel.Channel
import io.netty.channel.ChannelOption
import io.netty.channel.socket.SocketChannel
import io.netty.channel.socket.nio.NioSocketChannel
import kotlinx.coroutines.runBlocking
import us.myles.ViaVersion.api.PacketWrapper
import us.myles.ViaVersion.api.Via
import us.myles.ViaVersion.api.data.StoredObject
import us.myles.ViaVersion.api.data.UserConnection
import us.myles.ViaVersion.api.protocol.Protocol
import us.myles.ViaVersion.api.protocol.ProtocolPipeline
@ -16,9 +22,20 @@ import us.myles.ViaVersion.api.protocol.SimpleProtocol
import us.myles.ViaVersion.api.remapper.PacketRemapper
import us.myles.ViaVersion.api.type.Type
import us.myles.ViaVersion.packets.State
import java.math.BigInteger
import java.net.InetAddress
import java.net.InetSocketAddress
import java.security.KeyFactory
import java.security.MessageDigest
import java.security.PublicKey
import java.security.SecureRandom
import java.security.spec.X509EncodedKeySpec
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.logging.Logger
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import javax.naming.NameNotFoundException
import javax.naming.directory.InitialDirContext
@ -127,22 +144,20 @@ object CloudHeadProtocol : SimpleProtocol() {
}
}
})
this.registerOutgoing(State.LOGIN, 1, 1, object : PacketRemapper() {
// encryption request
override fun registerMap() {
handler {
val frontForwarder = it.user().channel!!.pipeline().get(CloudSideForwarder::class.java)
it.cancel()
frontForwarder.disconnect("Online mode in backend currently isn't compatible")
}
}
})
}
}
object CloudTailProtocol : SimpleProtocol() {
override fun registerPackets() {
// Login start
this.registerIncoming(State.LOGIN, 0, 0, object : PacketRemapper() {
override fun registerMap() {
handler {
it.user().get(CloudData::class.java)!!.frontLoginName = it.passthrough(Type.STRING)
}
}
})
this.registerOutgoing(State.LOGIN, 3, 3, object : PacketRemapper() {
// set compression
override fun registerMap() {
@ -162,6 +177,131 @@ object CloudTailProtocol : SimpleProtocol() {
}
}
})
// Crypto request
this.registerOutgoing(State.LOGIN, 1, 1, object : PacketRemapper() {
override fun registerMap() {
map(Type.STRING) // Server id - unused
map(Type.BYTE_ARRAY_PRIMITIVE) // Public key
map(Type.BYTE_ARRAY_PRIMITIVE) // Token
handler {
val data = it.user().get(CloudData::class.java)!!
data.backServerId = it.get(Type.STRING, 0)
data.backPublicKey = KeyFactory.getInstance("RSA")
.generatePublic(X509EncodedKeySpec(it.get(Type.BYTE_ARRAY_PRIMITIVE, 0)))
data.backToken = it.get(Type.BYTE_ARRAY_PRIMITIVE, 1)
// We'll use non-vanilla server id, public key size and token size
it.set(Type.STRING, 0, "VIAaaS")
it.set(Type.BYTE_ARRAY_PRIMITIVE, 0, mcCryptoKey.public.encoded)
val token = ByteArray(16)
secureRandom.nextBytes(token)
data.frontToken = token
it.set(Type.BYTE_ARRAY_PRIMITIVE, 1, token.clone())
}
}
})
this.registerIncoming(State.LOGIN, 1, 1, object : PacketRemapper() {
override fun registerMap() {
map(Type.BYTE_ARRAY_PRIMITIVE) // RSA shared secret
map(Type.BYTE_ARRAY_PRIMITIVE) // RSA token - wat??? why is it encrypted with RSA if it was sent unencrypted?
handler { wrapper ->
val data = wrapper.user().get(CloudData::class.java)!!
val encryptedSecret = wrapper.get(Type.BYTE_ARRAY_PRIMITIVE, 0)
val secret = Cipher.getInstance("RSA").let {
it.init(Cipher.DECRYPT_MODE, mcCryptoKey.private)
it.doFinal(encryptedSecret)
}
val encryptedToken = wrapper.get(Type.BYTE_ARRAY_PRIMITIVE, 1)
val decryptedToken = Cipher.getInstance("RSA").let {
it.init(Cipher.DECRYPT_MODE, mcCryptoKey.private)
it.doFinal(encryptedToken)
}!!
if (!decryptedToken.contentEquals(data.frontToken!!)) throw IllegalStateException("invalid token!")
val spec = SecretKeySpec(secret, "AES")
val iv = IvParameterSpec(secret)
val aesEn = Cipher.getInstance("AES/CFB8/NoPadding")
val aesDe = Cipher.getInstance("AES/CFB8/NoPadding")
aesEn.init(Cipher.ENCRYPT_MODE, spec, iv)
aesDe.init(Cipher.DECRYPT_MODE, spec, iv)
wrapper.user().channel!!.pipeline().get(CloudEncryptor::class.java).cipher = aesEn
wrapper.user().channel!!.pipeline().get(CloudDecryptor::class.java).cipher = aesDe
val frontHash = generateServerHash("VIAaaS", secret, mcCryptoKey.public)
val backKey = ByteArray(16)
secureRandom.nextBytes(backKey)
val backSpec = SecretKeySpec(secret, "AES")
val backIv = IvParameterSpec(secret)
val backAesEn = Cipher.getInstance("AES/CFB8/NoPadding")
val backAesDe = Cipher.getInstance("AES/CFB8/NoPadding")
backAesEn.init(Cipher.ENCRYPT_MODE, backSpec, backIv)
backAesDe.init(Cipher.DECRYPT_MODE, backSpec, backIv)
val backHash = generateServerHash(data.backServerId!!, backKey, data.backPublicKey!!)
wrapper.cancel()
Via.getPlatform().runAsync {
// Don't need to disable autoread, server will wait us
runBlocking {
try {
val profile = httpClient.get<JsonObject?>("https://sessionserver.mojang.com/session/minecraft/hasJoined?username=" +
"${UrlEscapers.urlFormParameterEscaper().escape(data.frontLoginName!!)}&serverId=$frontHash")
?: throw IllegalArgumentException("Couldn't authenticate with session servers")
var sent = false
viaWebServer.listeners[fromUndashed(profile.get("id")!!.asString)]?.forEach {
it.ws.send("""{"action": "session_hash_request", "session_hash": "$backHash",
| "client_address": "${wrapper.user().channel!!.remoteAddress()}", "backend_public_key":
| "${Base64.getEncoder().encodeToString(data.backPublicKey!!.encoded)}"}""".trimMargin())
it.ws.flush()
sent = true
}
if (!sent) {
throw IllegalStateException("No connection to browser, connect in /auth.html")
} else {
viaWebServer.pendingSessionHashes.get(backHash).get(15, TimeUnit.SECONDS)
wrapper.user().channel!!.eventLoop().submit {
val backCrypto = ByteBufAllocator.DEFAULT.buffer()
try {
backCrypto.writeByte(1) // Packet id
Type.BYTE_ARRAY_PRIMITIVE.write(backCrypto, Cipher.getInstance("RSA").let {
it.init(Cipher.ENCRYPT_MODE, data.backPublicKey)
it.doFinal(backKey)
})
Type.BYTE_ARRAY_PRIMITIVE.write(backCrypto, Cipher.getInstance("RSA").let {
it.init(Cipher.ENCRYPT_MODE, data.backPublicKey)
it.doFinal(data.backToken)
})
val backChan = wrapper.user().channel!!.pipeline()
.get(CloudSideForwarder::class.java).other!!
backChan.writeAndFlush(backCrypto.retain())
backChan.pipeline().get(CloudEncryptor::class.java).cipher = backAesEn
backChan.pipeline().get(CloudDecryptor::class.java).cipher = backAesDe
} finally {
backCrypto.release()
}
}
}
} catch (e: Exception) {
wrapper.user().channel!!.pipeline()
.get(CloudSideForwarder::class.java)
.disconnect("Online mode error: $e")
}
}
}
}
}
})
}
}
@ -169,3 +309,29 @@ fun Channel.setAutoRead(b: Boolean) {
this.config().isAutoRead = b
if (b) this.read()
}
val secureRandom = SecureRandom.getInstanceStrong()
fun twosComplementHexdigest(digest: ByteArray): String {
return BigInteger(digest).toString(16)
}
// https://github.com/VelocityPowered/Velocity/blob/0dd6fe1ef2783fe1f9322af06c6fd218aa67cdb1/proxy/src/main/java/com/velocitypowered/proxy/util/EncryptionUtils.java
fun generateServerHash(serverId: String, sharedSecret: ByteArray?, key: PublicKey): String {
val digest = MessageDigest.getInstance("SHA-1")
digest.update(serverId.toByteArray(Charsets.ISO_8859_1))
digest.update(sharedSecret)
digest.update(key.encoded)
return twosComplementHexdigest(digest.digest())
}
data class CloudData(val userConnection: UserConnection,
var backendVer: Int,
var frontOnline: Boolean,
var pendingStatus: Boolean = false,
var backServerId: String? = null,
var backPublicKey: PublicKey? = null,
var backToken: ByteArray? = null,
var frontToken: ByteArray? = null,
var frontLoginName: String? = null
) : StoredObject(userConnection)

View File

@ -21,6 +21,7 @@ import us.myles.ViaVersion.api.protocol.ProtocolVersion
import us.myles.ViaVersion.util.Config
import java.io.File
import java.net.InetAddress
import java.security.KeyPairGenerator
val httpClient = HttpClient {
defaultRequest {
@ -31,6 +32,12 @@ val httpClient = HttpClient {
}
}
// Minecraft doesn't have forward secrecy
val mcCryptoKey = KeyPairGenerator.getInstance("RSA").let {
it.initialize(4096) // https://stackoverflow.com/questions/1904516/is-1024-bit-rsa-secure
it.genKeyPair()
}
fun main(args: Array<String>) {
File("config/https.jks").apply {
parentFile.mkdirs()

View File

@ -1,9 +1,12 @@
package com.github.creeper123123321.viaaas
import com.google.common.base.Preconditions
import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader
import com.google.gson.Gson
import com.google.gson.JsonObject
import io.ktor.application.*
import io.ktor.client.request.*
import io.ktor.client.request.forms.*
import io.ktor.features.*
import io.ktor.http.*
@ -12,18 +15,22 @@ import io.ktor.http.content.*
import io.ktor.routing.*
import io.ktor.websocket.*
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.runBlocking
import java.net.URLEncoder
import java.time.Duration
import java.util.*
import java.util.UUID
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.TimeUnit
import kotlin.collections.set
// todo https://minecraft.id/documentation
class ViaWebApp {
val server = WebDashboardServer()
val viaWebServer = WebDashboardServer()
class ViaWebApp {
fun Application.main() {
install(DefaultHeaders)
install(CallLogging)
@ -34,17 +41,18 @@ class ViaWebApp {
routing {
webSocket("/ws") {
try {
server.connected(this)
viaWebServer.connected(this)
incoming.consumeEach { frame ->
if (frame is Frame.Text) {
server.onMessage(this, frame.readText())
viaWebServer.onMessage(this, frame.readText())
}
}
} catch (e: Exception) {
server.onException(this, e)
e.printStackTrace()
viaWebServer.onException(this, e)
this.close(CloseReason(CloseReason.Codes.INTERNAL_ERROR, e.toString()))
} finally {
server.disconnected(this)
viaWebServer.disconnected(this)
}
}
@ -56,12 +64,35 @@ class ViaWebApp {
}
}
// https://github.com/VelocityPowered/Velocity/blob/6467335f74a7d1617512a55cc9acef5e109b51ac/api/src/main/java/com/velocitypowered/api/util/UuidUtils.java
fun fromUndashed(string: String): UUID {
Preconditions.checkArgument(string.length == 32, "Length is incorrect")
return UUID(
java.lang.Long.parseUnsignedLong(string.substring(0, 16), 16),
java.lang.Long.parseUnsignedLong(string.substring(16), 16)
)
}
class WebDashboardServer {
val clients = ConcurrentHashMap<WebSocketSession, WebClient>()
val loginTokens = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.DAYS)
.build<UUID, String>()
val usernames = ConcurrentHashMap<String, WebClient>()
.build<UUID, UUID>()
// Minecraft account -> WebClient
val listeners = ConcurrentHashMap<UUID, MutableSet<WebClient>>()
val usernameIdCache = CacheBuilder.newBuilder()
.expireAfterWrite(1, TimeUnit.HOURS)
.build<String, UUID>(CacheLoader.from { name ->
runBlocking {
httpClient.get<JsonObject?>("https://api.mojang.com/users/profiles/minecraft/$name")
?.get("id")?.asString?.let { fromUndashed(it) }
}
})
val pendingSessionHashes = CacheBuilder.newBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS)
.build<String, CompletableFuture<Void>>(CacheLoader.from { _ -> CompletableFuture() })
suspend fun connected(ws: WebSocketSession) {
val loginState = WebLogin()
@ -91,7 +122,7 @@ class WebDashboardServer {
data class WebClient(val server: WebDashboardServer,
val ws: WebSocketSession,
val state: WebState,
val listenedUsernames: MutableSet<String> = mutableSetOf())
val listenedIds: MutableSet<UUID> = mutableSetOf())
interface WebState {
suspend fun start(webClient: WebClient)
@ -120,29 +151,34 @@ class WebLogin : WebState {
encodeInQuery = false) {
}
if (check.getAsJsonPrimitive("valid").asBoolean) {
val token = UUID.randomUUID()
webClient.server.loginTokens.put(token, username)
val mcIdUser = check.get("username").asString
val uuid = webClient.server.usernameIdCache.get(mcIdUser)
webClient.server.loginTokens.put(token, uuid)
webClient.ws.send("""{"action": "minecraft_id_result", "success": true,
| "username": "$username", "token": "$token"}""".trimMargin())
| "username": "$mcIdUser", "uuid": "$uuid", "token": "$token"}""".trimMargin())
} else {
webClient.ws.send("""{"action": "minecraft_id_result", "success": false}""")
}
}
"listen_login_requests" -> {
val token = UUID.fromString(obj.getAsJsonPrimitive("token").asString)
val user = webClient.server.loginTokens.get(token) { "" }
if (user != "") {
val user = webClient.server.loginTokens.getIfPresent(token)
if (user != null) {
webClient.ws.send("""{"action": "listen_login_requests_result", "token": "$token", "success": true, "username": "$user"}""")
webClient.listenedUsernames.add(user)
webClient.server.usernames[user] = webClient
webClient.listenedIds.add(user)
webClient.server.listeners.computeIfAbsent(user) { Collections.newSetFromMap(ConcurrentHashMap()) }
.add(webClient)
} else {
webClient.ws.send("""{"action": "listen_login_requests_result", "token": "$token", "success": false}""")
}
}
"session_hash_response" -> {
val token = UUID.fromString(obj.getAsJsonPrimitive("token").asString)
val user = webClient.server.loginTokens.get(token) { null }!!
val hash = obj.get("session_hash").asString
webClient.server.pendingSessionHashes.getIfPresent(hash)?.complete(null)
}
else -> throw IllegalStateException("invalid action!")
}
@ -151,7 +187,7 @@ class WebLogin : WebState {
}
override suspend fun disconnected(webClient: WebClient) {
webClient.listenedUsernames.forEach { webClient.server.usernames.remove(it, webClient) }
webClient.listenedIds.forEach { webClient.server.listeners[it]?.remove(webClient) }
}
override suspend fun onException(webClient: WebClient, exception: java.lang.Exception) {

View File

@ -3,15 +3,15 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VIAaaS</title>
<title>VIAaaS Authenticator</title>
<style>
body {font-family: sans-serif}
</style>
</head>
<body>
WIP todo insert here online mode auth code with wss, store login things in the browser, make viaaas ask the browser to contact session servers
WIP TODO - authenticate to backend server via browser, proxy the Mojang Login API, does this work on 1.7 and other versions?
<p>WebSocket connection status: <span id="connection_status">?</span></p>
<p>DO NOT TYPE YOUR CREDENTIALS IF YOU DON'T TRUST THE CONNECTION TO THIS VIAAAS INSTANCE!</p>
<p>DO NOT TYPE YOUR CREDENTIALS IF YOU DON'T TRUST THIS VIAAAS INSTANCE!</p>
<p><span id="content"></span></p>
<script>
let urlParams = new URLSearchParams();
@ -44,6 +44,8 @@ WIP todo insert here online mode auth code with wss, store login things in the b
socket.onopen = () => {
connectionStatus.innerText = "connected";
content.innerHTML = "";
(localStorage.getItem("tokens") || "").split(",").filter(it => it != "").forEach(listen);
};
socket.onclose = evt => {
@ -56,22 +58,24 @@ WIP todo insert here online mode auth code with wss, store login things in the b
console.log(event.data.toString());
let parsed = JSON.parse(event.data);
if (parsed.action == "ad_minecraft_id_login") {
if (username != null && mcauth_code != null) {
let add = document.createElement("a");
add.innerText = "Add account " + username;
add.href = "javascript:";
add.onclick = () => {
socket.send('{"action": "minecraft_id_login", "username": "' + username + '", "code": "' + mcauth_code + '"}');
};
content.appendChild(add);
}
let link = document.createElement("a");
link.innerText = "Prove your ownership of your accounts to this VIAaaS instance with Minecraft.ID";
link.innerText = "Add account with Minecraft.ID";
link.href = "javascript:";
link.onclick = () => {
if (username != null && mcauth_code != null) {
socket.send('{"action": "minecraft_id_login", "username": "' + username + '", "code": "' + mcauth_code + '"}');
username = null; mcauth_code = null;
} else {
let user = prompt("Username: ", "");
let callbackUrl = new URL(location.origin + location.pathname + "?username=" + encodeURIComponent(user));
location = "https://api.minecraft.id/gateway/start/" + encodeURIComponent(user) + "?callback=" + encodeURIComponent(callbackUrl);
}
let user = prompt("Username: ", "");
let callbackUrl = new URL(location.origin + location.pathname + "?username=" + encodeURIComponent(user));
location = "https://api.minecraft.id/gateway/start/" + encodeURIComponent(user) + "?callback=" + encodeURIComponent(callbackUrl);
};
content.appendChild(link);
(localStorage.getItem("tokens") || "").split(",").filter(it => it != "").forEach(listen);
} else if (parsed.action == "minecraft_id_result") {
if (!parsed.success) {
alert("Server couldn't verify account via Minecraft.ID");
@ -91,6 +95,9 @@ WIP todo insert here online mode auth code with wss, store login things in the b
tokens = tokens.filter(it => it != parsed.token);
localStorage.setItem("tokens", tokens.join(","));
}
} else if (parsed.action == "session_hash_request") {
alert("TODO!"); // todo
socket.send('{"action": "session_hash_response", "session_hash": "' + parsed.session_hash + '"}');
}
};
}